@oneuptime/common 14.0.10 → 14.0.11
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/AIConversation.ts +127 -1
- package/Models/DatabaseModels/CephCluster.ts +12 -0
- package/Models/DatabaseModels/CloudResource.ts +12 -0
- package/Models/DatabaseModels/DatabaseServer.ts +12 -0
- package/Models/DatabaseModels/DockerHost.ts +12 -0
- package/Models/DatabaseModels/DockerSwarmCluster.ts +12 -0
- package/Models/DatabaseModels/GlobalOidc.ts +0 -4
- package/Models/DatabaseModels/GlobalOidcProject.ts +0 -4
- package/Models/DatabaseModels/GlobalSso.ts +0 -4
- package/Models/DatabaseModels/GlobalSsoProject.ts +0 -4
- package/Models/DatabaseModels/Host.ts +12 -0
- package/Models/DatabaseModels/IncidentForm.ts +594 -0
- package/Models/DatabaseModels/IncidentFormSubmission.ts +283 -0
- package/Models/DatabaseModels/IncidentTemplate.ts +59 -0
- package/Models/DatabaseModels/Index.ts +14 -0
- package/Models/DatabaseModels/IoTFleet.ts +12 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +12 -0
- package/Models/DatabaseModels/MessageQueue.ts +1102 -0
- package/Models/DatabaseModels/MessageQueueLabelRule.ts +553 -0
- package/Models/DatabaseModels/MessageQueueOwnerRule.ts +633 -0
- package/Models/DatabaseModels/MessageQueueOwnerTeam.ts +491 -0
- package/Models/DatabaseModels/MessageQueueOwnerUser.ts +490 -0
- package/Models/DatabaseModels/PodmanHost.ts +12 -0
- package/Models/DatabaseModels/Project.ts +59 -0
- package/Models/DatabaseModels/ProjectOidc.ts +0 -4
- package/Models/DatabaseModels/ProjectSso.ts +0 -4
- package/Models/DatabaseModels/ProxmoxCluster.ts +12 -0
- package/Models/DatabaseModels/RumApplication.ts +12 -0
- package/Models/DatabaseModels/ServerlessFunction.ts +12 -0
- package/Models/DatabaseModels/Service.ts +12 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +0 -4
- package/Models/DatabaseModels/StatusPageSso.ts +0 -4
- package/Models/DatabaseModels/TeamComplianceSetting.ts +47 -2
- package/Models/DatabaseModels/VMwareVCenter.ts +12 -0
- package/Server/API/AIChatAPI.ts +13 -114
- package/Server/API/AIInvestigationAPI.ts +3 -3
- package/Server/API/AIInvestigationConversationAPI.ts +266 -0
- package/Server/API/IncidentFormAPI.ts +376 -0
- package/Server/API/ProjectOIDC.ts +16 -25
- package/Server/API/ProjectSSO.ts +16 -24
- package/Server/API/StatusPageAPI.ts +42 -77
- package/Server/Enterprise/EnterpriseEdition.ts +93 -28
- package/Server/Enterprise/EnterpriseFeature.ts +25 -3
- package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +1 -1
- package/Server/Enterprise/EnterpriseServerModule.ts +4 -1
- package/Server/EnvironmentConfig.ts +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.ts +129 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.ts +75 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.ts +477 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/IncidentFormRateLimit.ts +847 -0
- package/Server/Middleware/UserAuthorization.ts +16 -38
- package/Server/Services/AIService.ts +10 -13
- package/Server/Services/IncidentCustomFieldService.ts +112 -2
- package/Server/Services/IncidentEpisodeMemberService.ts +24 -4
- package/Server/Services/IncidentEpisodeService.ts +10 -1
- package/Server/Services/IncidentFormService.ts +1566 -0
- package/Server/Services/IncidentFormSubmissionService.ts +97 -0
- package/Server/Services/IncidentGroupingEngineService.ts +45 -8
- package/Server/Services/IncidentService.ts +123 -5
- package/Server/Services/IncidentTemplateService.ts +22 -0
- package/Server/Services/Index.ts +14 -0
- package/Server/Services/MessageQueueLabelRuleEngineService.ts +321 -0
- package/Server/Services/MessageQueueLabelRuleService.ts +14 -0
- package/Server/Services/MessageQueueOwnerRuleEngineService.ts +412 -0
- package/Server/Services/MessageQueueOwnerRuleService.ts +14 -0
- package/Server/Services/MessageQueueOwnerTeamService.ts +62 -0
- package/Server/Services/MessageQueueOwnerUserService.ts +62 -0
- package/Server/Services/MessageQueueService.ts +1463 -0
- package/Server/Services/OpenTelemetryIngestService.ts +37 -4
- package/Server/Services/ProjectService.ts +0 -59
- package/Server/Services/StatusPagePrivateUserService.ts +1 -4
- package/Server/Services/StatusPageService.ts +1 -51
- package/Server/Services/TeamComplianceSettingService.ts +163 -42
- package/Server/Services/UserNotificationRuleService.ts +8 -1
- package/Server/Services/WorkspaceNotificationSummaryService.ts +12 -3
- package/Server/Types/Database/Permissions/CreatePermission.ts +17 -3
- package/Server/Types/Database/Permissions/EditionPermission.ts +140 -38
- package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +16 -0
- package/Server/Types/Database/Permissions/TablePermission.ts +3 -3
- package/Server/Types/Database/Permissions/UpdatePermission.ts +20 -6
- package/Server/Utils/AI/Chat/AgentContextCompactor.ts +200 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +307 -48
- package/Server/Utils/AI/Chat/ChatRunCancellation.ts +156 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +78 -18
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
- package/Server/Utils/AI/Chat/ToolOutputPager.ts +322 -0
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +88 -30
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +13 -1
- package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +0 -32
- package/Server/Utils/AI/CodeFix/FixRunBudget.ts +27 -25
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +7 -1
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +34 -14
- package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +69 -24
- package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +9 -0
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +53 -10
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +27 -10
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +27 -10
- package/Server/Utils/AI/SRE/InvestigationQueue.ts +16 -19
- package/Server/Utils/AI/SRE/InvestigationThread.ts +646 -0
- package/Server/Utils/AI/SRE/InvestigationThreadService.ts +715 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +10 -3
- package/Server/Utils/EditionEnforcement.ts +19 -202
- package/Server/Utils/LLM/LLMService.ts +259 -63
- package/Server/Utils/RelaxedScimTeamLocksReport.ts +245 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
- package/Server/Utils/SameOriginRequest.ts +203 -0
- package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +4 -1
- package/Server/Utils/Telemetry/MessageQueueDiscovery.ts +1402 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +4 -1
- package/Server/Utils/VendorAssets.ts +3 -3
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +3 -1
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +3 -1
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +10 -1
- package/Tests/App/Accounts/IncidentFormPage.test.tsx +2036 -0
- package/Tests/App/Accounts/IncidentFormServerMessages.test.ts +119 -0
- package/Tests/App/Accounts/IncidentFormUtils.test.ts +1157 -0
- package/Tests/App/Accounts/LoginPasskey.test.tsx +6 -4
- package/Tests/App/Accounts/LoginSso.test.tsx +724 -0
- package/Tests/App/AdminDashboard/AdminAuthenticationRequireSso.test.tsx +338 -0
- package/Tests/App/AdminDashboard/AdminAuthenticationSsoCardLocales.test.tsx +331 -0
- package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +47 -33
- package/Tests/App/AdminDashboard/GlobalSsoOidcPages.test.tsx +1021 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +876 -78
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +20 -3
- package/Tests/App/Dashboard/CephProxmoxVMwareDocumentationCards.test.tsx +359 -0
- package/Tests/App/Dashboard/CephSetupGuide.test.ts +961 -0
- package/Tests/App/Dashboard/CloudDocumentationCard.test.tsx +381 -95
- package/Tests/App/Dashboard/CloudSetupGuide.test.ts +1463 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +174 -12
- package/Tests/App/Dashboard/ContainerSetupGuideCards.test.tsx +345 -0
- package/Tests/App/Dashboard/CoreListEmptyStates.test.tsx +501 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +0 -1
- package/Tests/App/Dashboard/DatabaseDocumentationCard.test.tsx +400 -0
- package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +5 -0
- package/Tests/App/Dashboard/DatabaseSetupGuide.test.ts +1869 -0
- package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +44 -0
- package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +50 -9
- package/Tests/App/Dashboard/DockerSetupGuide.test.ts +899 -0
- package/Tests/App/Dashboard/DockerSwarmSetupGuide.test.ts +964 -0
- package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +141 -7
- package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +52 -39
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +7 -1
- package/Tests/App/Dashboard/GettingStartedTaskRoutes.test.tsx +331 -0
- package/Tests/App/Dashboard/HomeOverviewStatsFirstRun.test.tsx +925 -0
- package/Tests/App/Dashboard/HostDocumentationCard.test.tsx +225 -0
- package/Tests/App/Dashboard/HostSetupGuide.test.ts +1183 -0
- package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +69 -45
- package/Tests/App/Dashboard/IncidentCreateTemplateCustomFieldSettings.test.tsx +745 -0
- package/Tests/App/Dashboard/IncidentCreateTemplateOwners.test.tsx +414 -0
- package/Tests/App/Dashboard/IncidentCustomFieldCreateSettingsForm.test.ts +1042 -0
- package/Tests/App/Dashboard/IncidentCustomFieldSettingsCard.test.tsx +1816 -0
- package/Tests/App/Dashboard/IncidentFormPlan.test.ts +89 -0
- package/Tests/App/Dashboard/IncidentFormShareLinkCard.test.tsx +846 -0
- package/Tests/App/Dashboard/IncidentFormsPages.test.tsx +1324 -0
- package/Tests/App/Dashboard/IncidentTemplateCustomFieldSettings.test.tsx +630 -0
- package/Tests/App/Dashboard/IncidentsSideMenu.test.tsx +13 -0
- package/Tests/App/Dashboard/IncomingEmailMonitorLinkVerifyHint.test.tsx +104 -0
- package/Tests/App/Dashboard/InvestigationConversation.test.tsx +1107 -0
- package/Tests/App/Dashboard/InvestigationConversationData.test.ts +546 -0
- package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +170 -5
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +207 -2
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +729 -34
- package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +9 -21
- package/Tests/App/Dashboard/InvestigationReportView.test.tsx +274 -37
- package/Tests/App/Dashboard/InvestigationStatusBadge.test.tsx +130 -0
- package/Tests/App/Dashboard/IoTDocumentationCard.test.tsx +218 -0
- package/Tests/App/Dashboard/IoTSetupGuide.test.ts +1124 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +20 -6
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +107 -83
- package/Tests/App/Dashboard/KubernetesSetupGuide.test.ts +787 -0
- package/Tests/App/Dashboard/MessageQueueBreadcrumbs.test.tsx +228 -0
- package/Tests/App/Dashboard/MessageQueueBrokerHealth.test.tsx +1538 -0
- package/Tests/App/Dashboard/MessageQueueBrokerHealthMetricChart.test.tsx +969 -0
- package/Tests/App/Dashboard/MessageQueueBrokerHealthReads.test.ts +889 -0
- package/Tests/App/Dashboard/MessageQueueBrokerMetricsReach.test.ts +236 -0
- package/Tests/App/Dashboard/MessageQueueDocumentationCard.test.tsx +479 -0
- package/Tests/App/Dashboard/MessageQueueListPages.test.tsx +1232 -0
- package/Tests/App/Dashboard/MessageQueueMetricDescriptions.test.ts +244 -0
- package/Tests/App/Dashboard/MessageQueueMonitorLink.test.ts +952 -0
- package/Tests/App/Dashboard/MessageQueueMonitorLinkMonitorCreate.test.tsx +859 -0
- package/Tests/App/Dashboard/MessageQueueOverview.test.tsx +1773 -0
- package/Tests/App/Dashboard/MessageQueueOverviewPresentation.test.ts +196 -0
- package/Tests/App/Dashboard/MessageQueuePresentation.test.ts +799 -0
- package/Tests/App/Dashboard/MessageQueueProducersConsumers.test.tsx +680 -0
- package/Tests/App/Dashboard/MessageQueueRoutes.test.tsx +383 -0
- package/Tests/App/Dashboard/MessageQueueRulePages.test.tsx +278 -0
- package/Tests/App/Dashboard/MessageQueueSetupGuide.test.ts +2778 -0
- package/Tests/App/Dashboard/MessageQueueSideMenus.test.tsx +273 -0
- package/Tests/App/Dashboard/MessageQueueSpanPopulations.test.ts +930 -0
- package/Tests/App/Dashboard/MessageQueueSpanStore.ts +351 -0
- package/Tests/App/Dashboard/MessageQueueTelemetryScope.test.ts +375 -0
- package/Tests/App/Dashboard/MessageQueueTelemetryScopeTabs.test.tsx +668 -0
- package/Tests/App/Dashboard/MessageQueueViewPages.test.tsx +685 -0
- package/Tests/App/Dashboard/MonitorAndHomeNotOperationalMenus.test.tsx +342 -0
- package/Tests/App/Dashboard/MonitorCreateFirstRunDefaults.test.tsx +623 -0
- package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +442 -0
- package/Tests/App/Dashboard/MonitorLogsPageViewSummary.test.tsx +814 -0
- package/Tests/App/Dashboard/MonitorSetupCardBodies.test.tsx +246 -0
- package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +8 -0
- package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +27 -12
- package/Tests/App/Dashboard/PlanGatedPage.test.tsx +292 -0
- package/Tests/App/Dashboard/PodmanSetupGuide.test.ts +948 -0
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +5 -4
- package/Tests/App/Dashboard/ProductSettingsSectionCollapsed.test.tsx +266 -0
- package/Tests/App/Dashboard/ProxmoxSetupGuide.test.ts +1318 -0
- package/Tests/App/Dashboard/ResourceAiAgentAttentionServerParity.test.ts +555 -0
- package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +298 -9
- package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +453 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +16 -0
- package/Tests/App/Dashboard/ResourceConnectionGuideCard.test.tsx +444 -0
- package/Tests/App/Dashboard/ResourceConnectionGuideOverviewsInfra.test.tsx +798 -0
- package/Tests/App/Dashboard/ResourceConnectionGuideOverviewsPlatforms.test.tsx +717 -0
- package/Tests/App/Dashboard/ResourceConnectionGuides.test.ts +981 -0
- package/Tests/App/Dashboard/ResourceConnectionState.test.ts +132 -0
- package/Tests/App/Dashboard/RumDocumentationCard.test.tsx +293 -0
- package/Tests/App/Dashboard/RumSessionReplayRetentionSettings.test.tsx +467 -0
- package/Tests/App/Dashboard/RumSetupGuide.test.ts +758 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +25 -0
- package/Tests/App/Dashboard/ServerlessDocumentationCard.test.tsx +254 -0
- package/Tests/App/Dashboard/ServerlessSetupGuide.test.ts +1074 -0
- package/Tests/App/Dashboard/SetupGuide.test.ts +246 -0
- package/Tests/App/Dashboard/SetupGuideCard.test.tsx +756 -0
- package/Tests/App/Dashboard/SloListSideMenu.test.tsx +120 -0
- package/Tests/App/Dashboard/SsoPages.test.tsx +833 -0
- package/Tests/App/Dashboard/TelemetryDocumentation.test.tsx +253 -0
- package/Tests/App/Dashboard/TelemetryRetentionShells.test.tsx +730 -0
- package/Tests/App/Dashboard/VMwareSetupGuide.test.ts +1033 -0
- package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +87 -7
- package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +28 -9
- package/Tests/Models/DatabaseModels/IncidentFormModel.test.ts +1250 -0
- package/Tests/Models/DatabaseModels/MessageQueueModels.test.ts +1983 -0
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +190 -14
- package/Tests/Models/IncidentTemplateCustomFieldSettingsColumn.test.ts +291 -0
- package/Tests/Server/API/AIInvestigationConversationAPI.test.ts +412 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +38 -9
- package/Tests/Server/API/IncidentFormAPI.test.ts +942 -0
- package/Tests/Server/API/IncidentFormPublicRoutes.test.ts +1653 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -1
- package/Tests/Server/API/{SsoProviderListingsEdition.test.ts → SsoProviderListings.test.ts} +164 -110
- package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +76 -54
- package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +125 -42
- package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +36 -10
- package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +51 -9
- package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +13 -8
- package/Tests/Server/Enterprise/SsoInCommunityEdition.test.ts +540 -0
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentFormsMigration.test.ts +438 -0
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleNotificationChannelsMigration.test.ts +255 -0
- package/Tests/Server/Infrastructure/Postgres/MessageQueueTablesMigration.test.ts +637 -0
- package/Tests/Server/Middleware/IncidentFormRateLimit.test.ts +1630 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +16 -17
- package/Tests/Server/Middleware/UserAuthorizationSsoEnforcement.test.ts +544 -0
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +37 -8
- package/Tests/Server/Services/IncidentCreatedFeedTitle.test.ts +212 -0
- package/Tests/Server/Services/IncidentCustomFieldDeleteFormQuestions.test.ts +297 -0
- package/Tests/Server/Services/IncidentFormOwnersCreatedNotification.test.ts +497 -0
- package/Tests/Server/Services/IncidentFormPostgres.test.ts +958 -0
- package/Tests/Server/Services/IncidentFormPublicForm.test.ts +1260 -0
- package/Tests/Server/Services/IncidentFormPublicSubmission.test.ts +2520 -0
- package/Tests/Server/Services/IncidentFormRegistration.test.ts +159 -0
- package/Tests/Server/Services/IncidentFormService.test.ts +663 -0
- package/Tests/Server/Services/IncidentFormSubmissionService.test.ts +430 -0
- package/Tests/Server/Services/IncidentFormSubmissionTemplateBranch.test.ts +473 -0
- package/Tests/Server/Services/IncidentGroupingTemplateSubstitution.test.ts +81 -0
- package/Tests/Server/Services/IncidentTemplateCustomFieldSettings.test.ts +262 -0
- package/Tests/Server/Services/IncidentTemplateOwnersOnDeclare.test.ts +566 -0
- package/Tests/Server/Services/IncidentTitleMarkdownSinks.test.ts +640 -0
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +3 -3
- package/Tests/Server/Services/MessageQueueOwnerServices.test.ts +440 -0
- package/Tests/Server/Services/MessageQueueRegistries.test.ts +577 -0
- package/Tests/Server/Services/MessageQueueRuleEngines.test.ts +900 -0
- package/Tests/Server/Services/MessageQueueService.test.ts +2210 -0
- package/Tests/Server/Services/MessageQueueSqlPostgres.test.ts +1271 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
- package/Tests/Server/Services/OpenTelemetryRetentionOverrideEdition.test.ts +474 -0
- package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +21 -0
- package/Tests/Server/Services/OwnerRowUniqueness.test.ts +3 -0
- package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +0 -1
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +56 -6
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -1
- package/Tests/Server/Services/SsoRequirementReads.test.ts +454 -0
- package/Tests/Server/Services/SsoRequirementWrites.test.ts +529 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
- package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +107 -89
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +1068 -176
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +1962 -200
- package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +654 -297
- package/Tests/Server/Types/Database/Permissions/RetentionOverrideColumns.test.ts +784 -0
- package/Tests/Server/Types/Database/Permissions/SsoScalePlanGate.test.ts +418 -0
- package/Tests/Server/Utils/AI/AIAlertGating.test.ts +0 -1
- package/Tests/Server/Utils/AI/AIInvestigationEngineRunLimits.test.ts +136 -0
- package/Tests/Server/Utils/AI/AIInvestigationQueue.test.ts +15 -14
- package/Tests/Server/Utils/AI/AgentContextCompactor.test.ts +351 -0
- package/Tests/Server/Utils/AI/ChatAgentRunnerSharedThread.test.ts +842 -0
- package/Tests/Server/Utils/AI/ChatRunCancellation.test.ts +195 -0
- package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +10 -57
- package/Tests/Server/Utils/AI/FixRunBudget.test.ts +20 -24
- package/Tests/Server/Utils/AI/Insights/InvestigationQueueInsightSubject.test.ts +7 -30
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +225 -29
- package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +266 -25
- package/Tests/Server/Utils/AI/InvestigationThread.test.ts +580 -0
- package/Tests/Server/Utils/AI/InvestigationThreadService.test.ts +801 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +525 -43
- package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +4 -2
- package/Tests/Server/Utils/AI/LLMServiceUnreachableProvider.test.ts +435 -0
- package/Tests/Server/Utils/AI/ObservabilityAssistantLimits.test.ts +462 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +14 -25
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +297 -15
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +10 -6
- package/Tests/Server/Utils/AI/ToolOutputPager.test.ts +382 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +23 -4
- package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +25 -6
- package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +14 -3
- package/Tests/Server/Utils/EditionEnforcement.test.ts +107 -147
- package/Tests/Server/Utils/{CommunityEditionSsoReport.test.ts → RelaxedScimTeamLocksReport.test.ts} +212 -208
- package/Tests/Server/Utils/SameOriginRequest.test.ts +372 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +20 -0
- package/Tests/Server/Utils/Telemetry/MessageQueueDiscovery.test.ts +2590 -0
- package/Tests/Server/Utils/Telemetry/MessageQueueSpanQueryTwin.ts +246 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +23 -0
- package/Tests/Types/AI/AIAgentRunLimits.test.ts +255 -0
- package/Tests/Types/Code/JsonSyntax.test.ts +1300 -0
- package/Tests/Types/CustomField/CustomFieldCreateSettings.test.ts +1113 -0
- package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +91 -0
- package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +30 -0
- package/Tests/Types/Incident/IncidentFormIpAllowlist.test.ts +227 -0
- package/Tests/Types/Incident/IncidentFormPublic.test.ts +1916 -0
- package/Tests/Types/MessageQueue/MessageQueueEntityKey.test.ts +916 -0
- package/Tests/Types/MessageQueue/MessageQueueIdentity.test.ts +567 -0
- package/Tests/Types/MessageQueue/MessageQueueManualIdentity.test.ts +686 -0
- package/Tests/Types/MessageQueue/MessageQueueMetricCatalog.test.ts +1726 -0
- package/Tests/Types/MessageQueue/MessageQueueMetricResolution.test.ts +1758 -0
- package/Tests/Types/MessageQueue/MessagingInstrumentationFixtures.test.ts +4686 -0
- package/Tests/Types/MessageQueue/MessagingSystem.test.ts +840 -0
- package/Tests/Types/MessageQueue/MessagingTelemetryFixtures.ts +3287 -0
- package/Tests/Types/MessageQueue/MessagingTelemetryResolver.test.ts +2966 -0
- package/Tests/Types/Monitor/MessageQueueAlertTemplates.test.ts +5401 -0
- package/Tests/Types/Monitor/MetricEngineFold.ts +154 -0
- package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +5 -3
- package/Tests/Types/Team/ComplianceRule.test.ts +588 -37
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +64 -11
- package/Tests/Types/XML.test.ts +51 -0
- package/Tests/UI/Components/CodeEditor/CodeEditor.test.tsx +2362 -0
- package/Tests/UI/Components/CodeEditor/CodeEditorCommands.test.ts +810 -0
- package/Tests/UI/Components/CodeEditor/CodeEditorHighlight.test.ts +338 -0
- package/Tests/UI/Components/CodeEditor/CodeEditorLanguages.test.ts +222 -0
- package/Tests/UI/Components/CodeEditor/CodeEditorStyles.test.ts +807 -0
- package/Tests/UI/Components/CodeEditor/NoMonaco.test.ts +245 -0
- package/Tests/UI/Components/CodeEditor/YamlEditor.test.tsx +729 -0
- package/Tests/UI/Components/CopyTextButton.test.tsx +76 -0
- package/Tests/UI/Components/EditionLabelLapseCopy.ts +51 -14
- package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +131 -23
- package/Tests/UI/Components/ErrorMessageAction.test.tsx +142 -0
- package/Tests/UI/Components/FacetSidebarInScope.test.tsx +459 -0
- package/Tests/UI/Components/FacetValuesInScope.test.ts +270 -0
- package/Tests/UI/Components/Forms/FormFieldCodeEditors.test.tsx +294 -0
- package/Tests/UI/Components/Forms/FormFieldMarkdown.test.tsx +126 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +22 -75
- package/Tests/UI/Components/Forms/ModelFormOnBeforeCreateMiscData.test.tsx +254 -0
- package/Tests/UI/Components/Icon.test.tsx +32 -0
- package/Tests/UI/Components/Input.test.tsx +31 -0
- package/Tests/UI/Components/LockedFilterChipScope.test.tsx +501 -0
- package/Tests/UI/Components/MarkdownConverters.test.ts +253 -1
- package/Tests/UI/Components/MarkdownEditor.test.tsx +2785 -2
- package/Tests/UI/Components/MarkdownEditorHistory.test.ts +243 -0
- package/Tests/UI/Components/MarkdownListEditing.test.ts +1118 -0
- package/Tests/UI/Components/MarkdownPaste.test.ts +1180 -0
- package/Tests/UI/Components/MarkdownViewerCopy.test.tsx +466 -0
- package/Tests/UI/Components/MarkdownVisualEditing.test.ts +1089 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableEmptyStateAction.test.tsx +623 -0
- package/Tests/UI/Components/ResetObjectID.test.tsx +282 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/Table/EmptyTableMessage.test.ts +439 -0
- package/Tests/UI/Components/Table/TableEmptyState.test.tsx +352 -0
- package/Tests/UI/Components/TableRowMobileTitles.test.tsx +78 -0
- package/Tests/UI/Components/TextArea.test.tsx +19 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +12 -29
- package/Tests/UI/Components/fixtures/MarkdownPasteFixtures.ts +460 -0
- package/Tests/UI/EsbuildConfig.test.ts +115 -39
- package/Tests/UI/EsbuildEnterpriseAlias.test.ts +4 -4
- package/Tests/UI/Monitor/MonitorLogSummaryRender.test.tsx +377 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +81 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +17 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
- package/Tests/Utils/Markdown/DashboardMarkdownRenderer.ts +81 -0
- package/Tests/Utils/Markdown/UntrustedMarkdown.test.ts +1339 -0
- package/Tests/Utils/Monitor/MonitorLogSummaryUtil.test.ts +862 -0
- package/Types/AI/AIAgentRunLimits.ts +146 -0
- package/Types/Code/CodeType.ts +2 -0
- package/Types/Code/JsonSyntax.ts +923 -0
- package/Types/CustomField/CustomFieldCreateSettings.ts +549 -0
- package/Types/CustomField/CustomFieldMappingCatalog.ts +77 -0
- package/Types/CustomField/CustomFieldValueValidator.ts +10 -7
- package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +1 -1
- package/Types/Icon/IconProp.ts +2 -0
- package/Types/Incident/IncidentFormIpAllowlist.ts +206 -0
- package/Types/Incident/IncidentFormPublic.ts +1145 -0
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +10 -3
- package/Types/MessageQueue/MessageQueueIdentity.ts +256 -0
- package/Types/MessageQueue/MessageQueueManualIdentity.ts +325 -0
- package/Types/MessageQueue/MessageQueueMetricCatalog.ts +1357 -0
- package/Types/MessageQueue/MessagingSystem.ts +518 -0
- package/Types/MessageQueue/MessagingTelemetryResolver.ts +2311 -0
- package/Types/Monitor/MessageQueueAlertTemplates.ts +2128 -0
- package/Types/Permission.ts +310 -0
- package/Types/ResourceAiAgent/ResourceAiAccess.ts +10 -4
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/Types/Team/ComplianceNotificationChannel.ts +4 -3
- package/Types/Team/ComplianceRule.ts +71 -18
- package/Types/Team/ComplianceRuleType.ts +2 -2
- package/Types/Team/TeamComplianceStatus.ts +6 -2
- package/Types/Telemetry/EntityType.ts +16 -0
- package/Types/Telemetry/LockedFilterDetail.ts +22 -0
- package/Types/XML.ts +26 -0
- package/UI/Components/CodeEditor/CodeEditor.css +379 -0
- package/UI/Components/CodeEditor/CodeEditor.tsx +1128 -294
- package/UI/Components/CodeEditor/CodeEditorCommands.ts +774 -0
- package/UI/Components/CodeEditor/CodeEditorHighlight.ts +106 -0
- package/UI/Components/CodeEditor/CodeEditorLanguages.ts +164 -0
- package/UI/Components/CodeEditor/YamlEditor.tsx +28 -326
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +5 -1
- package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +6 -25
- package/UI/Components/EditionLabel/EditionLabel.tsx +14 -13
- package/UI/Components/ErrorMessage/ErrorMessage.tsx +11 -1
- package/UI/Components/Forms/Fields/FormField.tsx +8 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Icon/Icon.tsx +18 -0
- package/UI/Components/Input/Input.tsx +11 -4
- package/UI/Components/LogsViewer/LogsViewer.tsx +4 -0
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +21 -3
- package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +877 -79
- package/UI/Components/Markdown.tsx/MarkdownEditorHistory.ts +284 -0
- package/UI/Components/Markdown.tsx/MarkdownListEditing.ts +1362 -0
- package/UI/Components/Markdown.tsx/MarkdownPaste.ts +1326 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +106 -10
- package/UI/Components/Markdown.tsx/MarkdownVisualEditing.ts +1032 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +85 -7
- package/UI/Components/ResetObjectID/ResetObjectID.tsx +35 -9
- package/UI/Components/Table/EmptyTableMessage.ts +81 -0
- package/UI/Components/Table/Table.tsx +50 -21
- package/UI/Components/Table/TableRow.tsx +9 -1
- package/UI/Components/TelemetryViewer/FacetVisibility.ts +97 -2
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +13 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +214 -7
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +21 -3
- package/UI/Components/TextArea/TextArea.tsx +9 -2
- package/UI/Components/Workflow/ModelFieldPicker.tsx +10 -12
- package/UI/Styles/Theme.css +8 -0
- package/UI/esbuild-config.js +2 -32
- package/Utils/LabelRuleImportExport.ts +2 -0
- package/Utils/Markdown/UntrustedMarkdown.ts +1513 -0
- package/Utils/Monitor/IncomingEmailMonitorRequestUtil.ts +41 -0
- package/Utils/Monitor/MonitorLogSummaryUtil.ts +212 -0
- package/Utils/Telemetry/EntityKey.ts +48 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js +125 -1
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CephCluster.js +12 -0
- package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +12 -0
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseServer.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +12 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +12 -0
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +0 -4
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +0 -4
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +0 -4
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +0 -4
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +12 -0
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentForm.js +605 -0
- package/build/dist/Models/DatabaseModels/IncidentForm.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFormSubmission.js +299 -0
- package/build/dist/Models/DatabaseModels/IncidentFormSubmission.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +60 -0
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +14 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IoTFleet.js +12 -0
- package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +12 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MessageQueue.js +1106 -0
- package/build/dist/Models/DatabaseModels/MessageQueue.js.map +1 -0
- package/build/dist/Models/DatabaseModels/MessageQueueLabelRule.js +562 -0
- package/build/dist/Models/DatabaseModels/MessageQueueLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/MessageQueueOwnerRule.js +643 -0
- package/build/dist/Models/DatabaseModels/MessageQueueOwnerRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/MessageQueueOwnerTeam.js +506 -0
- package/build/dist/Models/DatabaseModels/MessageQueueOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/MessageQueueOwnerUser.js +505 -0
- package/build/dist/Models/DatabaseModels/MessageQueueOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/PodmanHost.js +12 -0
- package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +61 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProjectOidc.js +0 -4
- package/build/dist/Models/DatabaseModels/ProjectOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProjectSso.js +0 -4
- package/build/dist/Models/DatabaseModels/ProjectSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +12 -0
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +12 -0
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServerlessFunction.js +12 -0
- package/build/dist/Models/DatabaseModels/ServerlessFunction.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Service.js +12 -0
- package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +0 -4
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSso.js +0 -4
- package/build/dist/Models/DatabaseModels/StatusPageSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +49 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js +12 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +9 -103
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +3 -3
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationConversationAPI.js +162 -0
- package/build/dist/Server/API/AIInvestigationConversationAPI.js.map +1 -0
- package/build/dist/Server/API/IncidentFormAPI.js +245 -0
- package/build/dist/Server/API/IncidentFormAPI.js.map +1 -0
- package/build/dist/Server/API/ProjectOIDC.js +16 -24
- package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
- package/build/dist/Server/API/ProjectSSO.js +16 -24
- package/build/dist/Server/API/ProjectSSO.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +42 -75
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Enterprise/EnterpriseEdition.js +79 -25
- package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -1
- package/build/dist/Server/Enterprise/EnterpriseFeature.js +24 -3
- package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -1
- package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +1 -1
- package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.js +63 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.js +183 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/IncidentFormRateLimit.js +559 -0
- package/build/dist/Server/Middleware/IncidentFormRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/UserAuthorization.js +11 -30
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +10 -13
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +83 -0
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeMemberService.js +8 -4
- package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js +10 -1
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/IncidentFormService.js +1202 -0
- package/build/dist/Server/Services/IncidentFormService.js.map +1 -0
- package/build/dist/Server/Services/IncidentFormSubmissionService.js +91 -0
- package/build/dist/Server/Services/IncidentFormSubmissionService.js.map +1 -0
- package/build/dist/Server/Services/IncidentGroupingEngineService.js +20 -8
- package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +106 -4
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentTemplateService.js +18 -0
- package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +14 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MessageQueueLabelRuleEngineService.js +262 -0
- package/build/dist/Server/Services/MessageQueueLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/MessageQueueLabelRuleService.js +13 -0
- package/build/dist/Server/Services/MessageQueueLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/MessageQueueOwnerRuleEngineService.js +334 -0
- package/build/dist/Server/Services/MessageQueueOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/MessageQueueOwnerRuleService.js +13 -0
- package/build/dist/Server/Services/MessageQueueOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/MessageQueueOwnerTeamService.js +62 -0
- package/build/dist/Server/Services/MessageQueueOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/MessageQueueOwnerUserService.js +62 -0
- package/build/dist/Server/Services/MessageQueueOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/MessageQueueService.js +1140 -0
- package/build/dist/Server/Services/MessageQueueService.js.map +1 -0
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +25 -4
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +0 -54
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -4
- package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +0 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +120 -31
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +8 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +12 -3
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +6 -3
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +109 -37
- package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +16 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js +3 -3
- package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +9 -6
- package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/AgentContextCompactor.js +128 -0
- package/build/dist/Server/Utils/AI/Chat/AgentContextCompactor.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +192 -50
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatRunCancellation.js +133 -0
- package/build/dist/Server/Utils/AI/Chat/ChatRunCancellation.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +53 -13
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
- package/build/dist/Server/Utils/AI/Chat/ToolOutputPager.js +206 -0
- package/build/dist/Server/Utils/AI/Chat/ToolOutputPager.js.map +1 -0
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +53 -26
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +4 -2
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +1 -29
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js +24 -24
- package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +7 -2
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +29 -14
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +38 -14
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +2 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +40 -10
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +19 -7
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +19 -7
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js +15 -18
- package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationThread.js +412 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationThread.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationThreadService.js +495 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationThreadService.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +10 -3
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
- package/build/dist/Server/Utils/EditionEnforcement.js +20 -158
- package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +193 -54
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/RelaxedScimTeamLocksReport.js +160 -0
- package/build/dist/Server/Utils/RelaxedScimTeamLocksReport.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/SameOriginRequest.js +151 -0
- package/build/dist/Server/Utils/SameOriginRequest.js.map +1 -0
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +4 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/MessageQueueDiscovery.js +1015 -0
- package/build/dist/Server/Utils/Telemetry/MessageQueueDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +4 -1
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/VendorAssets.js +3 -3
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +3 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +3 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +10 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIAgentRunLimits.js +91 -0
- package/build/dist/Types/AI/AIAgentRunLimits.js.map +1 -0
- package/build/dist/Types/Code/CodeType.js +2 -0
- package/build/dist/Types/Code/CodeType.js.map +1 -1
- package/build/dist/Types/Code/JsonSyntax.js +604 -0
- package/build/dist/Types/Code/JsonSyntax.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldCreateSettings.js +310 -0
- package/build/dist/Types/CustomField/CustomFieldCreateSettings.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +38 -0
- package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js +10 -7
- package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +1 -1
- package/build/dist/Types/Icon/IconProp.js +2 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Incident/IncidentFormIpAllowlist.js +119 -0
- package/build/dist/Types/Incident/IncidentFormIpAllowlist.js.map +1 -0
- package/build/dist/Types/Incident/IncidentFormPublic.js +678 -0
- package/build/dist/Types/Incident/IncidentFormPublic.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +10 -3
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/MessageQueue/MessageQueueIdentity.js +184 -0
- package/build/dist/Types/MessageQueue/MessageQueueIdentity.js.map +1 -0
- package/build/dist/Types/MessageQueue/MessageQueueManualIdentity.js +190 -0
- package/build/dist/Types/MessageQueue/MessageQueueManualIdentity.js.map +1 -0
- package/build/dist/Types/MessageQueue/MessageQueueMetricCatalog.js +1057 -0
- package/build/dist/Types/MessageQueue/MessageQueueMetricCatalog.js.map +1 -0
- package/build/dist/Types/MessageQueue/MessagingSystem.js +394 -0
- package/build/dist/Types/MessageQueue/MessagingSystem.js.map +1 -0
- package/build/dist/Types/MessageQueue/MessagingTelemetryResolver.js +1768 -0
- package/build/dist/Types/MessageQueue/MessagingTelemetryResolver.js.map +1 -0
- package/build/dist/Types/Monitor/MessageQueueAlertTemplates.js +1207 -0
- package/build/dist/Types/Monitor/MessageQueueAlertTemplates.js.map +1 -0
- package/build/dist/Types/Permission.js +274 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +10 -4
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +12 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +4 -3
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -1
- package/build/dist/Types/Team/ComplianceRule.js +48 -14
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -1
- package/build/dist/Types/Team/ComplianceRuleType.js +2 -2
- package/build/dist/Types/Telemetry/EntityType.js +16 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/XML.js +23 -0
- package/build/dist/Types/XML.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +641 -266
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditorCommands.js +464 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditorCommands.js.map +1 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditorHighlight.js +69 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditorHighlight.js.map +1 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditorLanguages.js +108 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditorLanguages.js.map +1 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +14 -186
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +5 -14
- package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +14 -13
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +1 -1
- package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +8 -3
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +8 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +11 -3
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +2 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +14 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +710 -65
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditorHistory.js +238 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditorHistory.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownListEditing.js +1028 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownListEditing.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownPaste.js +1088 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownPaste.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +70 -10
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownVisualEditing.js +887 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownVisualEditing.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +57 -6
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js +7 -5
- package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js.map +1 -1
- package/build/dist/UI/Components/Table/EmptyTableMessage.js +51 -0
- package/build/dist/UI/Components/Table/EmptyTableMessage.js.map +1 -0
- package/build/dist/UI/Components/Table/Table.js +30 -11
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +9 -2
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +63 -1
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +4 -2
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +91 -10
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +14 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +9 -3
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ModelFieldPicker.js +1 -2
- package/build/dist/UI/Components/Workflow/ModelFieldPicker.js.map +1 -1
- package/build/dist/Utils/LabelRuleImportExport.js +2 -0
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Markdown/UntrustedMarkdown.js +988 -0
- package/build/dist/Utils/Markdown/UntrustedMarkdown.js.map +1 -0
- package/build/dist/Utils/Monitor/IncomingEmailMonitorRequestUtil.js +34 -0
- package/build/dist/Utils/Monitor/IncomingEmailMonitorRequestUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorLogSummaryUtil.js +148 -0
- package/build/dist/Utils/Monitor/MonitorLogSummaryUtil.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +36 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/package.json +1 -3
- package/Server/Utils/CommunityEditionSsoReport.ts +0 -361
- package/Tests/App/Accounts/LoginSsoEdition.test.tsx +0 -616
- package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +0 -217
- package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +0 -271
- package/Tests/App/Dashboard/CloudDocumentationMarkdown.test.ts +0 -215
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +0 -784
- package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +0 -518
- package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +0 -419
- package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +0 -769
- package/Tests/UI/Components/CodeEditor.test.tsx +0 -638
- package/Tests/UI/Components/MonacoLoader.test.ts +0 -67
- package/Tests/UI/Components/MonacoRuntime.test.ts +0 -121
- package/Tests/UI/Components/Telemetry/TelemetryRetentionConfigForm.test.tsx +0 -329
- package/Tests/UI/Components/YamlEditor.test.tsx +0 -847
- package/UI/Components/CodeEditor/MonacoLoader.ts +0 -36
- package/UI/Components/Telemetry/TelemetryRetentionConfigForm.tsx +0 -319
- package/UI/Components/Telemetry/TelemetryRetentionConfigSummary.tsx +0 -223
- package/build/dist/Server/Utils/CommunityEditionSsoReport.js +0 -223
- package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +0 -1
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +0 -34
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +0 -1
- package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js +0 -171
- package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js.map +0 -1
- package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigSummary.js +0 -119
- package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigSummary.js.map +0 -1
|
@@ -0,0 +1,1653 @@
|
|
|
1
|
+
jest.mock("../../../Server/Infrastructure/Redis", () => {
|
|
2
|
+
return {
|
|
3
|
+
__esModule: true,
|
|
4
|
+
default: {
|
|
5
|
+
getClient: jest.fn(),
|
|
6
|
+
isConnected: jest.fn(),
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
12
|
+
return {
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: {
|
|
15
|
+
debug: jest.fn(),
|
|
16
|
+
info: jest.fn(),
|
|
17
|
+
warn: jest.fn(),
|
|
18
|
+
error: jest.fn(),
|
|
19
|
+
},
|
|
20
|
+
getLogAttributesFromRequest: jest.fn().mockReturnValue({}),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
import IncidentFormAPI, {
|
|
25
|
+
INCIDENT_FORM_FOREIGN_PAGE_MESSAGE,
|
|
26
|
+
INCIDENT_FORM_SUBMISSION_BODY_MESSAGE,
|
|
27
|
+
} from "../../../Server/API/IncidentFormAPI";
|
|
28
|
+
import { EncryptionSecret } from "../../../Server/EnvironmentConfig";
|
|
29
|
+
import Redis from "../../../Server/Infrastructure/Redis";
|
|
30
|
+
import {
|
|
31
|
+
INCIDENT_FORM_RATE_LIMIT_UNAVAILABLE_MESSAGE,
|
|
32
|
+
INCIDENT_FORM_READ_RATE_LIMIT_MESSAGE,
|
|
33
|
+
INCIDENT_FORM_SUBMIT_RATE_LIMIT_MESSAGE,
|
|
34
|
+
INCIDENT_FORM_TOTAL_RATE_LIMIT_MESSAGE,
|
|
35
|
+
} from "../../../Server/Middleware/IncidentFormRateLimit";
|
|
36
|
+
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
37
|
+
import AccessTokenService from "../../../Server/Services/AccessTokenService";
|
|
38
|
+
import IncidentCustomFieldService from "../../../Server/Services/IncidentCustomFieldService";
|
|
39
|
+
import IncidentFormService, {
|
|
40
|
+
INCIDENT_FORM_NETWORK_NOT_ALLOWED_MESSAGE,
|
|
41
|
+
INCIDENT_FORM_NOT_AVAILABLE_MESSAGE,
|
|
42
|
+
} from "../../../Server/Services/IncidentFormService";
|
|
43
|
+
import IncidentFormSubmissionService from "../../../Server/Services/IncidentFormSubmissionService";
|
|
44
|
+
import IncidentInternalNoteService from "../../../Server/Services/IncidentInternalNoteService";
|
|
45
|
+
import IncidentService from "../../../Server/Services/IncidentService";
|
|
46
|
+
import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
|
|
47
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
48
|
+
import UserService from "../../../Server/Services/UserService";
|
|
49
|
+
import CaptchaUtil from "../../../Server/Utils/Captcha";
|
|
50
|
+
import CookieUtil from "../../../Server/Utils/Cookie";
|
|
51
|
+
import { ExpressRouter } from "../../../Server/Utils/Express";
|
|
52
|
+
import JSONWebToken from "../../../Server/Utils/JsonWebToken";
|
|
53
|
+
import SameOriginRequest from "../../../Server/Utils/SameOriginRequest";
|
|
54
|
+
import { expressErrorHandler } from "../../../Server/Utils/StartServer";
|
|
55
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
56
|
+
import IncidentForm from "../../../Models/DatabaseModels/IncidentForm";
|
|
57
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
58
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
59
|
+
import Email from "../../../Types/Email";
|
|
60
|
+
import {
|
|
61
|
+
INCIDENT_FORM_PAGE_HEADER,
|
|
62
|
+
INCIDENT_FORM_PAGE_HEADER_VALUE,
|
|
63
|
+
IncidentFormFieldSetting,
|
|
64
|
+
} from "../../../Types/Incident/IncidentFormPublic";
|
|
65
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
66
|
+
import Name from "../../../Types/Name";
|
|
67
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
68
|
+
import {
|
|
69
|
+
afterAll,
|
|
70
|
+
afterEach,
|
|
71
|
+
beforeAll,
|
|
72
|
+
beforeEach,
|
|
73
|
+
describe,
|
|
74
|
+
expect,
|
|
75
|
+
it,
|
|
76
|
+
} from "@jest/globals";
|
|
77
|
+
import CookieParser from "cookie-parser";
|
|
78
|
+
import express from "express";
|
|
79
|
+
import http from "http";
|
|
80
|
+
import jwt from "jsonwebtoken";
|
|
81
|
+
import { AddressInfo } from "net";
|
|
82
|
+
import timers from "timers";
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* A handler's refusal travels as next(err), and Express leaves a router
|
|
86
|
+
* whose layers are exhausted with setImmediate(done, err). Common's jest
|
|
87
|
+
* environment is jsdom, which has no setImmediate - without this every
|
|
88
|
+
* refusal would hang the request instead of reaching the error handler. Lend
|
|
89
|
+
* jsdom Node's own, as MultipartFormData.test.ts does.
|
|
90
|
+
*/
|
|
91
|
+
if (
|
|
92
|
+
typeof (globalThis as unknown as { setImmediate?: unknown }).setImmediate !==
|
|
93
|
+
"function"
|
|
94
|
+
) {
|
|
95
|
+
(globalThis as unknown as { setImmediate: unknown }).setImmediate =
|
|
96
|
+
timers.setImmediate;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* The public incident form routes, driven through a real Express app with
|
|
101
|
+
* cookie-parser, the JSON body parser and the production error handler -
|
|
102
|
+
* the real IncidentFormAPI router, the real limiter, the real user
|
|
103
|
+
* middleware and the real IncidentFormService. Only the data layer (and the
|
|
104
|
+
* captcha provider and Redis) are stubbed.
|
|
105
|
+
*
|
|
106
|
+
* It pins what the public form page has to handle - the status codes and
|
|
107
|
+
* the messages - and the one property the page depends on most: a visitor's
|
|
108
|
+
* own OneUptime session cookie, which rides along because the page is served
|
|
109
|
+
* on the dashboard's host, never turns into a 401. A 401 would send the
|
|
110
|
+
* dashboard client to the login page (losing the report being typed), or
|
|
111
|
+
* sign the visitor out of their real session. A cookie that no longer
|
|
112
|
+
* decodes, an expired one and a junk one are all simply ignored, and a
|
|
113
|
+
* valid one is ignored too: forms are anonymous.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
117
|
+
"5d7f3c0a-4c55-4d3e-9a3e-2d4a7d1c9f01",
|
|
118
|
+
);
|
|
119
|
+
const OTHER_PROJECT_ID: string = "5d7f3c0a-4c55-4d3e-9a3e-2d4a7d1c9f02";
|
|
120
|
+
const FORM_ID: string = "a1b2c3d4-0000-4000-8000-0000000000f1";
|
|
121
|
+
const SHARE_KEY: string = "7c9e6679-7425-40de-944b-e07fc1f90ae7";
|
|
122
|
+
const OFF_SHARE_KEY: string = "7c9e6679-7425-40de-944b-e07fc1f90ae8";
|
|
123
|
+
const LOCKED_SHARE_KEY: string = "7c9e6679-7425-40de-944b-e07fc1f90ae9";
|
|
124
|
+
const UNKNOWN_SHARE_KEY: string = "0f8fad5b-d9cb-469f-a165-70867728950e";
|
|
125
|
+
const SEVERITY_ID: string = "b0000000-0000-4000-8000-0000000000f1";
|
|
126
|
+
const INCIDENT_ID: string = "e0000000-0000-4000-8000-0000000000f1";
|
|
127
|
+
const USER_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
|
|
128
|
+
|
|
129
|
+
// The client address our proxy would append, and one outside the allowlist.
|
|
130
|
+
const ALLOWED_IP: string = "198.51.100.23";
|
|
131
|
+
const OTHER_IP: string = "203.0.113.7";
|
|
132
|
+
|
|
133
|
+
// This instance, as HTTP_PROTOCOL and HOST configure it, and another site.
|
|
134
|
+
const INSTANCE_ORIGIN: string = "https://oneuptime.example.com";
|
|
135
|
+
const FOREIGN_ORIGIN: string = "https://evil.example";
|
|
136
|
+
|
|
137
|
+
const INVALID_ACCESS_TOKEN_MESSAGE: string =
|
|
138
|
+
"AccessToken is invalid or expired. Please refresh your token.";
|
|
139
|
+
|
|
140
|
+
type MockedFn = ReturnType<typeof jest.fn>;
|
|
141
|
+
|
|
142
|
+
const getClientMock: MockedFn = Redis.getClient as unknown as MockedFn;
|
|
143
|
+
const isConnectedMock: MockedFn = Redis.isConnected as unknown as MockedFn;
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* A counting Redis fake. `countFloor` makes every counter read at least
|
|
147
|
+
* that much, which is how a test puts a caller over a limit without firing
|
|
148
|
+
* hundreds of requests first.
|
|
149
|
+
*/
|
|
150
|
+
class FakeRedisClient {
|
|
151
|
+
public counters: Map<string, number> = new Map();
|
|
152
|
+
public countFloor: number = 0;
|
|
153
|
+
|
|
154
|
+
// A pipeline counting a key this matches fails, as a broken Redis would.
|
|
155
|
+
public failKeysMatching: RegExp | null = null;
|
|
156
|
+
|
|
157
|
+
public pipeline(): FakePipeline {
|
|
158
|
+
return new FakePipeline(this);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public keysMatching(fragment: string): Array<string> {
|
|
162
|
+
return Array.from(this.counters.keys()).filter((key: string) => {
|
|
163
|
+
return key.includes(fragment);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type QueuedCommand = () => [Error | null, unknown];
|
|
169
|
+
|
|
170
|
+
class FakePipeline {
|
|
171
|
+
private commands: Array<QueuedCommand> = [];
|
|
172
|
+
private keys: Array<string> = [];
|
|
173
|
+
|
|
174
|
+
public constructor(private client: FakeRedisClient) {}
|
|
175
|
+
|
|
176
|
+
public incr(key: string): FakePipeline {
|
|
177
|
+
this.keys.push(key);
|
|
178
|
+
this.commands.push((): [Error | null, unknown] => {
|
|
179
|
+
const next: number = Math.max(
|
|
180
|
+
(this.client.counters.get(key) || 0) + 1,
|
|
181
|
+
this.client.countFloor,
|
|
182
|
+
);
|
|
183
|
+
this.client.counters.set(key, next);
|
|
184
|
+
return [null, next];
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public expire(): FakePipeline {
|
|
191
|
+
this.commands.push((): [Error | null, unknown] => {
|
|
192
|
+
return [null, 1];
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public async exec(): Promise<unknown> {
|
|
199
|
+
const failKeysMatching: RegExp | null = this.client.failKeysMatching;
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
failKeysMatching &&
|
|
203
|
+
this.keys.some((key: string): boolean => {
|
|
204
|
+
return failKeysMatching.test(key);
|
|
205
|
+
})
|
|
206
|
+
) {
|
|
207
|
+
throw new Error("connection reset");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.commands.map((command: QueuedCommand) => {
|
|
211
|
+
return command();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildForm(data: {
|
|
217
|
+
shareKey: string;
|
|
218
|
+
isEnabled?: boolean;
|
|
219
|
+
ipWhitelist?: string;
|
|
220
|
+
}): IncidentForm {
|
|
221
|
+
const form: IncidentForm = new IncidentForm();
|
|
222
|
+
form._id = FORM_ID;
|
|
223
|
+
form.projectId = PROJECT_ID;
|
|
224
|
+
form.name = "Report a Problem";
|
|
225
|
+
form.description = "Tell us what is broken.";
|
|
226
|
+
form.isEnabled = data.isEnabled !== false;
|
|
227
|
+
form.shareKey = new ObjectID(data.shareKey);
|
|
228
|
+
form.incidentSeverityId = new ObjectID(SEVERITY_ID);
|
|
229
|
+
form.allowReporterToChooseSeverity = false;
|
|
230
|
+
form.descriptionSetting = IncidentFormFieldSetting.Optional;
|
|
231
|
+
form.customFieldSettings = {};
|
|
232
|
+
form.isReporterDetailsRequired = true;
|
|
233
|
+
form.successMessage = "Thanks - we are on it.";
|
|
234
|
+
form.ipWhitelist = data.ipWhitelist || "";
|
|
235
|
+
return form;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const FORMS: Array<IncidentForm> = [
|
|
239
|
+
buildForm({ shareKey: SHARE_KEY }),
|
|
240
|
+
buildForm({ shareKey: OFF_SHARE_KEY, isEnabled: false }),
|
|
241
|
+
buildForm({ shareKey: LOCKED_SHARE_KEY, ipWhitelist: "198.51.100.0/24" }),
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const PUBLIC_FORM: JSONObject = {
|
|
245
|
+
name: "Report a Problem",
|
|
246
|
+
description: "Tell us what is broken.",
|
|
247
|
+
descriptionSetting: "Optional",
|
|
248
|
+
isReporterDetailsRequired: true,
|
|
249
|
+
customFields: [],
|
|
250
|
+
isCaptchaRequired: false,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const ANSWERS: JSONObject = {
|
|
254
|
+
title: "Checkout is down",
|
|
255
|
+
description: "Every order fails.",
|
|
256
|
+
reporterName: "Jane Doe",
|
|
257
|
+
reporterEmail: "jane@example.com",
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const SUBMISSION_RESULT: JSONObject = {
|
|
261
|
+
incidentNumber: "INC-42",
|
|
262
|
+
successMessage: "Thanks - we are on it.",
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Signed with a secret this instance no longer uses: a rotation happened.
|
|
266
|
+
const STALE_ACCESS_TOKEN: string = jwt.sign(
|
|
267
|
+
{
|
|
268
|
+
userId: USER_ID.toString(),
|
|
269
|
+
email: "viewer@example.com",
|
|
270
|
+
name: "Viewer",
|
|
271
|
+
isMasterAdmin: false,
|
|
272
|
+
isGlobalLogin: true,
|
|
273
|
+
sessionId: "44444444-4444-4444-8444-444444444444",
|
|
274
|
+
},
|
|
275
|
+
`${EncryptionSecret.toString()}-before-rotation`,
|
|
276
|
+
{ expiresIn: 15 * 60 },
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Signed with the right secret, but past its lifetime.
|
|
280
|
+
const EXPIRED_ACCESS_TOKEN: string = jwt.sign(
|
|
281
|
+
{
|
|
282
|
+
userId: USER_ID.toString(),
|
|
283
|
+
email: "viewer@example.com",
|
|
284
|
+
name: "Viewer",
|
|
285
|
+
isMasterAdmin: false,
|
|
286
|
+
isGlobalLogin: true,
|
|
287
|
+
sessionId: "44444444-4444-4444-8444-444444444444",
|
|
288
|
+
exp: Math.floor(Date.now() / 1000) - 60,
|
|
289
|
+
},
|
|
290
|
+
EncryptionSecret.toString(),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
function validAccessToken(): string {
|
|
294
|
+
return JSONWebToken.signUserLoginToken({
|
|
295
|
+
tokenData: {
|
|
296
|
+
userId: USER_ID,
|
|
297
|
+
email: new Email("viewer@example.com"),
|
|
298
|
+
name: new Name("Viewer"),
|
|
299
|
+
timezone: null,
|
|
300
|
+
isMasterAdmin: false,
|
|
301
|
+
isGlobalLogin: true,
|
|
302
|
+
sessionId: ObjectID.generate(),
|
|
303
|
+
},
|
|
304
|
+
expiresInSeconds: 15 * 60,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface HttpResult {
|
|
309
|
+
status: number;
|
|
310
|
+
headers: http.IncomingHttpHeaders;
|
|
311
|
+
body: JSONObject | null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function send(data: {
|
|
315
|
+
port: number;
|
|
316
|
+
method: "GET" | "POST";
|
|
317
|
+
path: string;
|
|
318
|
+
clientIp?: string;
|
|
319
|
+
cookies?: Dictionary<string> | undefined;
|
|
320
|
+
headers?: http.OutgoingHttpHeaders | undefined;
|
|
321
|
+
/*
|
|
322
|
+
* Without the header the form page's API client adds to every request:
|
|
323
|
+
* what an <img>, a link, a frame or a no-cors fetch sends.
|
|
324
|
+
*/
|
|
325
|
+
withoutPageHeader?: boolean | undefined;
|
|
326
|
+
body?: unknown;
|
|
327
|
+
// Sent as it is, with the content-type the headers give: not JSON.
|
|
328
|
+
rawBody?: string | undefined;
|
|
329
|
+
}): Promise<HttpResult> {
|
|
330
|
+
return new Promise<HttpResult>(
|
|
331
|
+
(resolve: (result: HttpResult) => void, reject: (e: Error) => void) => {
|
|
332
|
+
const payload: string =
|
|
333
|
+
data.method === "POST" && data.body !== undefined
|
|
334
|
+
? JSON.stringify(data.body)
|
|
335
|
+
: "";
|
|
336
|
+
|
|
337
|
+
const headers: http.OutgoingHttpHeaders = {
|
|
338
|
+
// What the public form page's API client sends.
|
|
339
|
+
tenantid: "",
|
|
340
|
+
...(data.withoutPageHeader
|
|
341
|
+
? {}
|
|
342
|
+
: { [INCIDENT_FORM_PAGE_HEADER]: INCIDENT_FORM_PAGE_HEADER_VALUE }),
|
|
343
|
+
// What our proxy appends: the client address, the trusted hop.
|
|
344
|
+
"x-forwarded-for": data.clientIp || ALLOWED_IP,
|
|
345
|
+
...(data.headers || {}),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const cookieHeader: string = Object.entries(data.cookies || {})
|
|
349
|
+
.map(([name, value]: [string, string]) => {
|
|
350
|
+
return `${name}=${value}`;
|
|
351
|
+
})
|
|
352
|
+
.join("; ");
|
|
353
|
+
|
|
354
|
+
if (cookieHeader) {
|
|
355
|
+
headers["cookie"] = cookieHeader;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (payload) {
|
|
359
|
+
headers["content-type"] = "application/json";
|
|
360
|
+
headers["content-length"] = Buffer.byteLength(payload);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (data.rawBody !== undefined) {
|
|
364
|
+
headers["content-length"] = Buffer.byteLength(data.rawBody);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const request: http.ClientRequest = http.request(
|
|
368
|
+
{
|
|
369
|
+
host: "127.0.0.1",
|
|
370
|
+
port: data.port,
|
|
371
|
+
path: data.path,
|
|
372
|
+
method: data.method,
|
|
373
|
+
headers,
|
|
374
|
+
},
|
|
375
|
+
(response: http.IncomingMessage) => {
|
|
376
|
+
const chunks: Array<Buffer> = [];
|
|
377
|
+
|
|
378
|
+
response.on("data", (chunk: Buffer) => {
|
|
379
|
+
chunks.push(chunk);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
response.on("end", () => {
|
|
383
|
+
const raw: string = Buffer.concat(chunks).toString("utf8");
|
|
384
|
+
let body: JSONObject | null = null;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
body = raw ? (JSON.parse(raw) as JSONObject) : null;
|
|
388
|
+
} catch {
|
|
389
|
+
body = { raw };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
resolve({
|
|
393
|
+
status: response.statusCode || 0,
|
|
394
|
+
headers: response.headers,
|
|
395
|
+
body,
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
request.on("error", reject);
|
|
402
|
+
|
|
403
|
+
if (payload) {
|
|
404
|
+
request.write(payload);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (data.rawBody !== undefined) {
|
|
408
|
+
request.write(data.rawBody);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
request.end();
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/*
|
|
417
|
+
* A limiter refusal is written by Response.sendErrorResponse ({ message });
|
|
418
|
+
* a handler's refusal goes through the production error handler ({ error }).
|
|
419
|
+
* The page's API client reads either.
|
|
420
|
+
*/
|
|
421
|
+
function errorMessageOf(result: HttpResult): unknown {
|
|
422
|
+
return result.body?.["message"] ?? result.body?.["error"];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Only what the page would see: the status and the body.
|
|
426
|
+
function seen(result: HttpResult): { status: number; body: unknown } {
|
|
427
|
+
return { status: result.status, body: result.body };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
describe("the public incident form routes over HTTP", () => {
|
|
431
|
+
let server: http.Server;
|
|
432
|
+
let port: number;
|
|
433
|
+
let client: FakeRedisClient;
|
|
434
|
+
let incidentCreate: MockedFn;
|
|
435
|
+
let getPublicForm: MockedFn;
|
|
436
|
+
let submitPublicForm: MockedFn;
|
|
437
|
+
let onPlan: MockedFn;
|
|
438
|
+
|
|
439
|
+
const readPath: (shareKey: string) => string = (shareKey: string) => {
|
|
440
|
+
return `/api/incident-form/public/${shareKey}`;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const submitPath: (shareKey: string) => string = (shareKey: string) => {
|
|
444
|
+
return `/api/incident-form/public/${shareKey}/submit`;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
beforeAll(async () => {
|
|
448
|
+
const app: express.Express = express();
|
|
449
|
+
app.use(CookieParser());
|
|
450
|
+
app.use(express.json());
|
|
451
|
+
/*
|
|
452
|
+
* As StartServer mounts it: "data[title]=..." from a plain HTML form
|
|
453
|
+
* becomes the very object a JSON body gives.
|
|
454
|
+
*/
|
|
455
|
+
app.use(express.urlencoded({ extended: true }));
|
|
456
|
+
app.use("/api", new IncidentFormAPI().getRouter());
|
|
457
|
+
app.use(expressErrorHandler);
|
|
458
|
+
|
|
459
|
+
server = http.createServer(app);
|
|
460
|
+
|
|
461
|
+
await new Promise<void>((resolve: () => void) => {
|
|
462
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
port = (server.address() as AddressInfo).port;
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
afterAll(async () => {
|
|
469
|
+
await new Promise<void>((resolve: () => void) => {
|
|
470
|
+
server.close(() => {
|
|
471
|
+
resolve();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
beforeEach(() => {
|
|
477
|
+
client = new FakeRedisClient();
|
|
478
|
+
getClientMock.mockReturnValue(client);
|
|
479
|
+
isConnectedMock.mockReturnValue(true);
|
|
480
|
+
|
|
481
|
+
jest
|
|
482
|
+
.spyOn(IncidentFormService, "findOneBy")
|
|
483
|
+
.mockImplementation((async (findBy: {
|
|
484
|
+
query: { shareKey?: ObjectID };
|
|
485
|
+
}): Promise<IncidentForm | null> => {
|
|
486
|
+
return (
|
|
487
|
+
FORMS.find((form: IncidentForm) => {
|
|
488
|
+
return (
|
|
489
|
+
form.shareKey?.toString() === findBy.query.shareKey?.toString()
|
|
490
|
+
);
|
|
491
|
+
}) || null
|
|
492
|
+
);
|
|
493
|
+
}) as never);
|
|
494
|
+
|
|
495
|
+
jest.spyOn(IncidentCustomFieldService, "findBy").mockResolvedValue([]);
|
|
496
|
+
jest.spyOn(IncidentSeverityService, "findBy").mockResolvedValue([]);
|
|
497
|
+
|
|
498
|
+
/*
|
|
499
|
+
* The plan check reads the project's subscription, which this suite has
|
|
500
|
+
* no database for. Left real, it would pass only where billing is off:
|
|
501
|
+
* CI's Common job runs with BILLING_ENABLED=true, the read fails, the
|
|
502
|
+
* check fails closed, and every form here would answer "not available".
|
|
503
|
+
* The check itself is pinned in IncidentFormPublicForm.test.ts; the
|
|
504
|
+
* off-plan test below turns it the other way.
|
|
505
|
+
*/
|
|
506
|
+
onPlan = jest
|
|
507
|
+
.spyOn(IncidentFormService, "isProjectOnPlan")
|
|
508
|
+
.mockResolvedValue(true as never) as unknown as MockedFn;
|
|
509
|
+
|
|
510
|
+
// HOST is not configured in a unit run; the instance says it is this.
|
|
511
|
+
jest
|
|
512
|
+
.spyOn(SameOriginRequest, "getInstanceOrigin")
|
|
513
|
+
.mockReturnValue(INSTANCE_ORIGIN);
|
|
514
|
+
|
|
515
|
+
incidentCreate = jest
|
|
516
|
+
.spyOn(IncidentService, "create")
|
|
517
|
+
.mockImplementation((async (): Promise<Incident> => {
|
|
518
|
+
const incident: Incident = new Incident();
|
|
519
|
+
incident._id = INCIDENT_ID;
|
|
520
|
+
incident.projectId = PROJECT_ID;
|
|
521
|
+
incident.incidentNumber = 42;
|
|
522
|
+
incident.incidentNumberWithPrefix = "INC-42";
|
|
523
|
+
return incident;
|
|
524
|
+
}) as never) as unknown as MockedFn;
|
|
525
|
+
|
|
526
|
+
jest
|
|
527
|
+
.spyOn(IncidentFormSubmissionService, "create")
|
|
528
|
+
.mockImplementation((async (createBy: { data: unknown }) => {
|
|
529
|
+
return createBy.data;
|
|
530
|
+
}) as never);
|
|
531
|
+
jest
|
|
532
|
+
.spyOn(IncidentInternalNoteService, "create")
|
|
533
|
+
.mockImplementation((async (createBy: { data: unknown }) => {
|
|
534
|
+
return createBy.data;
|
|
535
|
+
}) as never);
|
|
536
|
+
|
|
537
|
+
jest.spyOn(CaptchaUtil, "isCaptchaEnabled").mockReturnValue(false);
|
|
538
|
+
|
|
539
|
+
/*
|
|
540
|
+
* What a signed-in visitor's valid session touches in the user
|
|
541
|
+
* middleware. Nothing here reads the database.
|
|
542
|
+
*/
|
|
543
|
+
jest.spyOn(UserService, "isUserBlocked").mockResolvedValue(false);
|
|
544
|
+
jest.spyOn(UserService, "updateLastActive").mockResolvedValue(undefined);
|
|
545
|
+
jest
|
|
546
|
+
.spyOn(AccessTokenService, "getUserGlobalAccessPermission")
|
|
547
|
+
.mockResolvedValue(null);
|
|
548
|
+
jest.spyOn(ProjectService, "updateLastActive").mockResolvedValue(undefined);
|
|
549
|
+
|
|
550
|
+
getPublicForm = jest.spyOn(
|
|
551
|
+
IncidentFormService,
|
|
552
|
+
"getPublicForm",
|
|
553
|
+
) as unknown as MockedFn;
|
|
554
|
+
submitPublicForm = jest.spyOn(
|
|
555
|
+
IncidentFormService,
|
|
556
|
+
"submitPublicForm",
|
|
557
|
+
) as unknown as MockedFn;
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
afterEach(() => {
|
|
561
|
+
jest.restoreAllMocks();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe("200", () => {
|
|
565
|
+
it("serves the form's questions, uncached", async () => {
|
|
566
|
+
const result: HttpResult = await send({
|
|
567
|
+
port,
|
|
568
|
+
method: "GET",
|
|
569
|
+
path: readPath(SHARE_KEY),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(seen(result)).toEqual({ status: 200, body: PUBLIC_FORM });
|
|
573
|
+
expect(result.headers["cache-control"]).toBe(
|
|
574
|
+
"no-store, no-cache, must-revalidate",
|
|
575
|
+
);
|
|
576
|
+
expect(result.headers["pragma"]).toBe("no-cache");
|
|
577
|
+
|
|
578
|
+
// The plan gate stays on the path; only its database read is stubbed.
|
|
579
|
+
expect(onPlan).toHaveBeenCalledTimes(1);
|
|
580
|
+
expect(String(onPlan.mock.calls[0]![0])).toBe(PROJECT_ID.toString());
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("declares the incident and answers with its number and the success message", async () => {
|
|
584
|
+
const result: HttpResult = await send({
|
|
585
|
+
port,
|
|
586
|
+
method: "POST",
|
|
587
|
+
path: submitPath(SHARE_KEY),
|
|
588
|
+
body: { data: ANSWERS },
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
expect(seen(result)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
592
|
+
expect(result.headers["cache-control"]).toBe(
|
|
593
|
+
"no-store, no-cache, must-revalidate",
|
|
594
|
+
);
|
|
595
|
+
expect(incidentCreate).toHaveBeenCalledTimes(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
/*
|
|
599
|
+
* The tenant the user middleware reads from a header or the body is
|
|
600
|
+
* never where the incident goes: that is always the form's project.
|
|
601
|
+
*/
|
|
602
|
+
it("declares in the form's project whatever tenant the request names", async () => {
|
|
603
|
+
await send({
|
|
604
|
+
port,
|
|
605
|
+
method: "POST",
|
|
606
|
+
path: submitPath(SHARE_KEY),
|
|
607
|
+
headers: { tenantid: OTHER_PROJECT_ID },
|
|
608
|
+
body: { data: ANSWERS, projectId: OTHER_PROJECT_ID },
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const created: Incident = (
|
|
612
|
+
incidentCreate.mock.calls[0]![0] as { data: Incident }
|
|
613
|
+
).data;
|
|
614
|
+
|
|
615
|
+
expect(created.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("takes the share key in any case", async () => {
|
|
619
|
+
const result: HttpResult = await send({
|
|
620
|
+
port,
|
|
621
|
+
method: "GET",
|
|
622
|
+
path: readPath(SHARE_KEY.toUpperCase()),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
expect(result.status).toBe(200);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe("400", () => {
|
|
630
|
+
it("names every problem with the answers, and declares nothing", async () => {
|
|
631
|
+
const result: HttpResult = await send({
|
|
632
|
+
port,
|
|
633
|
+
method: "POST",
|
|
634
|
+
path: submitPath(SHARE_KEY),
|
|
635
|
+
body: { data: { title: " ", reporterEmail: "jane" } },
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(result.status).toBe(400);
|
|
639
|
+
expect(errorMessageOf(result)).toBe(
|
|
640
|
+
"Title is required. Your Name is required. Your Email is not a valid email address.",
|
|
641
|
+
);
|
|
642
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it.each([
|
|
646
|
+
["no body", undefined],
|
|
647
|
+
["a body without answers", { title: "Down" }],
|
|
648
|
+
["answers that are not an object", { data: "Down" }],
|
|
649
|
+
])("refuses %s", async (_label: string, body: unknown) => {
|
|
650
|
+
const result: HttpResult = await send({
|
|
651
|
+
port,
|
|
652
|
+
method: "POST",
|
|
653
|
+
path: submitPath(SHARE_KEY),
|
|
654
|
+
body,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
expect(result.status).toBe(400);
|
|
658
|
+
expect(errorMessageOf(result)).toBe(
|
|
659
|
+
INCIDENT_FORM_SUBMISSION_BODY_MESSAGE,
|
|
660
|
+
);
|
|
661
|
+
expect(submitPublicForm).not.toHaveBeenCalled();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
/*
|
|
665
|
+
* The provider round trip is the one thing stubbed here: its refusal is
|
|
666
|
+
* a BadDataException, exactly as CaptchaUtil throws it.
|
|
667
|
+
*/
|
|
668
|
+
it("refuses a submission the captcha provider rejects, when the instance has a captcha on", async () => {
|
|
669
|
+
jest.spyOn(CaptchaUtil, "isCaptchaEnabled").mockReturnValue(true);
|
|
670
|
+
const verifyCaptcha: MockedFn = jest
|
|
671
|
+
.spyOn(CaptchaUtil, "verifyCaptcha")
|
|
672
|
+
.mockRejectedValue(
|
|
673
|
+
new BadDataException(
|
|
674
|
+
"Captcha verification failed. Please try again.",
|
|
675
|
+
),
|
|
676
|
+
) as unknown as MockedFn;
|
|
677
|
+
|
|
678
|
+
const result: HttpResult = await send({
|
|
679
|
+
port,
|
|
680
|
+
method: "POST",
|
|
681
|
+
path: submitPath(SHARE_KEY),
|
|
682
|
+
body: { data: ANSWERS, captchaToken: "solved-token" },
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
expect(result.status).toBe(400);
|
|
686
|
+
expect(errorMessageOf(result)).toBe(
|
|
687
|
+
"Captcha verification failed. Please try again.",
|
|
688
|
+
);
|
|
689
|
+
expect(verifyCaptcha).toHaveBeenCalledWith({
|
|
690
|
+
token: "solved-token",
|
|
691
|
+
remoteIp: ALLOWED_IP,
|
|
692
|
+
});
|
|
693
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("declares once the captcha provider accepts the token", async () => {
|
|
697
|
+
jest.spyOn(CaptchaUtil, "isCaptchaEnabled").mockReturnValue(true);
|
|
698
|
+
jest.spyOn(CaptchaUtil, "verifyCaptcha").mockResolvedValue(undefined);
|
|
699
|
+
|
|
700
|
+
const result: HttpResult = await send({
|
|
701
|
+
port,
|
|
702
|
+
method: "POST",
|
|
703
|
+
path: submitPath(SHARE_KEY),
|
|
704
|
+
body: { data: ANSWERS, captchaToken: "solved-token" },
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
expect(seen(result)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
describe("403", () => {
|
|
712
|
+
it("refuses to show a form to a network it does not allow", async () => {
|
|
713
|
+
const result: HttpResult = await send({
|
|
714
|
+
port,
|
|
715
|
+
method: "GET",
|
|
716
|
+
path: readPath(LOCKED_SHARE_KEY),
|
|
717
|
+
clientIp: OTHER_IP,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
expect(result.status).toBe(403);
|
|
721
|
+
expect(errorMessageOf(result)).toBe(
|
|
722
|
+
INCIDENT_FORM_NETWORK_NOT_ALLOWED_MESSAGE,
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("refuses a submission from a network it does not allow", async () => {
|
|
727
|
+
const result: HttpResult = await send({
|
|
728
|
+
port,
|
|
729
|
+
method: "POST",
|
|
730
|
+
path: submitPath(LOCKED_SHARE_KEY),
|
|
731
|
+
clientIp: OTHER_IP,
|
|
732
|
+
body: { data: ANSWERS },
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
expect(result.status).toBe(403);
|
|
736
|
+
expect(errorMessageOf(result)).toBe(
|
|
737
|
+
INCIDENT_FORM_NETWORK_NOT_ALLOWED_MESSAGE,
|
|
738
|
+
);
|
|
739
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
/*
|
|
743
|
+
* The address checked is the one our proxy appended, not one the caller
|
|
744
|
+
* wrote into X-Forwarded-For themselves.
|
|
745
|
+
*/
|
|
746
|
+
it("cannot be talked round by a forged X-Forwarded-For entry", async () => {
|
|
747
|
+
const result: HttpResult = await send({
|
|
748
|
+
port,
|
|
749
|
+
method: "GET",
|
|
750
|
+
path: readPath(LOCKED_SHARE_KEY),
|
|
751
|
+
clientIp: `${ALLOWED_IP}, ${OTHER_IP}`,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
expect(result.status).toBe(403);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("serves an allowed network", async () => {
|
|
758
|
+
const result: HttpResult = await send({
|
|
759
|
+
port,
|
|
760
|
+
method: "GET",
|
|
761
|
+
path: readPath(LOCKED_SHARE_KEY),
|
|
762
|
+
clientIp: ALLOWED_IP,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
expect(result.status).toBe(200);
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
/*
|
|
770
|
+
* Any website can have a visitor's browser send these requests - the API
|
|
771
|
+
* answers every origin, and a plain HTML form or an <img> needs no
|
|
772
|
+
* preflight - from the visitor's own address: inside the form's IP
|
|
773
|
+
* allowlist, and on the budget everybody behind that address shares. Such
|
|
774
|
+
* a request is refused before anything counts it or looks the link up.
|
|
775
|
+
* The form's own page is served, and so is a caller that is not a browser
|
|
776
|
+
* but sends what the page sends; a read must carry the page's header and
|
|
777
|
+
* a submission's body must be JSON, whoever sends them.
|
|
778
|
+
*/
|
|
779
|
+
describe("a request another site's page had a browser send", () => {
|
|
780
|
+
// What a plain HTML form on another site posts.
|
|
781
|
+
const FORGED_FORM: string =
|
|
782
|
+
"data%5Btitle%5D=Forged+outage&data%5BreporterName%5D=CEO&data%5BreporterEmail%5D=ceo%40corp.example";
|
|
783
|
+
|
|
784
|
+
const OWN_PAGE: http.OutgoingHttpHeaders = {
|
|
785
|
+
origin: INSTANCE_ORIGIN,
|
|
786
|
+
"sec-fetch-site": "same-origin",
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
it("refuses a plain HTML form another site auto-posts from inside the allowlist, before anything counts it", async () => {
|
|
790
|
+
const result: HttpResult = await send({
|
|
791
|
+
port,
|
|
792
|
+
method: "POST",
|
|
793
|
+
path: submitPath(LOCKED_SHARE_KEY),
|
|
794
|
+
clientIp: ALLOWED_IP,
|
|
795
|
+
headers: {
|
|
796
|
+
origin: FOREIGN_ORIGIN,
|
|
797
|
+
"sec-fetch-site": "cross-site",
|
|
798
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
799
|
+
},
|
|
800
|
+
rawBody: FORGED_FORM,
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
expect(result.status).toBe(403);
|
|
804
|
+
expect(errorMessageOf(result)).toBe(INCIDENT_FORM_FOREIGN_PAGE_MESSAGE);
|
|
805
|
+
expect(submitPublicForm).not.toHaveBeenCalled();
|
|
806
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
807
|
+
expect(client.counters.size).toBe(0);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// A preflighted JSON request carries the same Origin.
|
|
811
|
+
it("refuses the same request sent as JSON: the body's type is not what stops it", async () => {
|
|
812
|
+
const result: HttpResult = await send({
|
|
813
|
+
port,
|
|
814
|
+
method: "POST",
|
|
815
|
+
path: submitPath(LOCKED_SHARE_KEY),
|
|
816
|
+
clientIp: ALLOWED_IP,
|
|
817
|
+
headers: { origin: FOREIGN_ORIGIN, "sec-fetch-site": "cross-site" },
|
|
818
|
+
body: { data: ANSWERS },
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
expect(result.status).toBe(403);
|
|
822
|
+
expect(errorMessageOf(result)).toBe(INCIDENT_FORM_FOREIGN_PAGE_MESSAGE);
|
|
823
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
824
|
+
expect(client.counters.size).toBe(0);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it.each([
|
|
828
|
+
["a cross-site read", { "sec-fetch-site": "cross-site" }],
|
|
829
|
+
["a read from another site's page", { origin: FOREIGN_ORIGIN }],
|
|
830
|
+
["a read from an opaque origin (a sandboxed frame)", { origin: "null" }],
|
|
831
|
+
/*
|
|
832
|
+
* A status page on its owner's domain, or a DNS-rebinding name pointed
|
|
833
|
+
* at this server: the browser calls it same-origin, but it is not
|
|
834
|
+
* this instance's page.
|
|
835
|
+
*/
|
|
836
|
+
[
|
|
837
|
+
"a read from another host the browser calls same-origin",
|
|
838
|
+
{
|
|
839
|
+
origin: "https://status.customer.example",
|
|
840
|
+
"sec-fetch-site": "same-origin",
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
])(
|
|
844
|
+
"refuses %s, before anything counts it",
|
|
845
|
+
async (_label: string, headers: http.OutgoingHttpHeaders) => {
|
|
846
|
+
const result: HttpResult = await send({
|
|
847
|
+
port,
|
|
848
|
+
method: "GET",
|
|
849
|
+
path: readPath(SHARE_KEY),
|
|
850
|
+
headers: headers,
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
expect(result.status).toBe(403);
|
|
854
|
+
expect(errorMessageOf(result)).toBe(INCIDENT_FORM_FOREIGN_PAGE_MESSAGE);
|
|
855
|
+
expect(getPublicForm).not.toHaveBeenCalled();
|
|
856
|
+
expect(client.counters.size).toBe(0);
|
|
857
|
+
},
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
/*
|
|
861
|
+
* Thirty-one posts to a made-up link from another site's page, through
|
|
862
|
+
* one visitor's browser: counted, they would have used up the 30 per 15
|
|
863
|
+
* minutes everybody behind that address shares across every form.
|
|
864
|
+
*/
|
|
865
|
+
it("cannot use up the budget of the address it sends from", async () => {
|
|
866
|
+
for (let attempt: number = 0; attempt < 31; attempt++) {
|
|
867
|
+
const refused: HttpResult = await send({
|
|
868
|
+
port,
|
|
869
|
+
method: "POST",
|
|
870
|
+
path: submitPath(ObjectID.generate().toString()),
|
|
871
|
+
clientIp: ALLOWED_IP,
|
|
872
|
+
headers: { origin: FOREIGN_ORIGIN, "sec-fetch-site": "cross-site" },
|
|
873
|
+
body: { data: ANSWERS },
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
expect(refused.status).toBe(403);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const colleague: HttpResult = await send({
|
|
880
|
+
port,
|
|
881
|
+
method: "POST",
|
|
882
|
+
path: submitPath(SHARE_KEY),
|
|
883
|
+
clientIp: ALLOWED_IP,
|
|
884
|
+
headers: OWN_PAGE,
|
|
885
|
+
body: { data: ANSWERS },
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
expect(seen(colleague)).toEqual({
|
|
889
|
+
status: 200,
|
|
890
|
+
body: SUBMISSION_RESULT,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
/*
|
|
894
|
+
* Only the colleague's own report was counted: once by address, once
|
|
895
|
+
* by form and address, and once against the form's ceiling.
|
|
896
|
+
*/
|
|
897
|
+
expect(
|
|
898
|
+
client
|
|
899
|
+
.keysMatching(`:submit:i:${ALLOWED_IP}:`)
|
|
900
|
+
.map((key: string): number => {
|
|
901
|
+
return client.counters.get(key)!;
|
|
902
|
+
}),
|
|
903
|
+
).toEqual([1]);
|
|
904
|
+
expect(Array.from(client.counters.values())).toEqual([1, 1, 1]);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it("serves the form's own page: its read and its report", async () => {
|
|
908
|
+
const read: HttpResult = await send({
|
|
909
|
+
port,
|
|
910
|
+
method: "GET",
|
|
911
|
+
path: readPath(SHARE_KEY),
|
|
912
|
+
headers: OWN_PAGE,
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
expect(seen(read)).toEqual({ status: 200, body: PUBLIC_FORM });
|
|
916
|
+
|
|
917
|
+
const report: HttpResult = await send({
|
|
918
|
+
port,
|
|
919
|
+
method: "POST",
|
|
920
|
+
path: submitPath(SHARE_KEY),
|
|
921
|
+
headers: OWN_PAGE,
|
|
922
|
+
body: { data: ANSWERS },
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
expect(seen(report)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
926
|
+
expect(incidentCreate).toHaveBeenCalledTimes(1);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
/*
|
|
930
|
+
* Over plain HTTP - the self-hosting default - a browser sends another
|
|
931
|
+
* site's <img>, link or no-cors fetch with neither Origin nor
|
|
932
|
+
* Sec-Fetch-Site, so the check above cannot see where it came from. It
|
|
933
|
+
* cannot add a header, though, and the form's page always does.
|
|
934
|
+
*/
|
|
935
|
+
it("refuses a read another site's <img> sends over plain HTTP, from inside the allowlist, before anything counts it", async () => {
|
|
936
|
+
const result: HttpResult = await send({
|
|
937
|
+
port,
|
|
938
|
+
method: "GET",
|
|
939
|
+
path: readPath(LOCKED_SHARE_KEY),
|
|
940
|
+
clientIp: ALLOWED_IP,
|
|
941
|
+
withoutPageHeader: true,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
expect(result.status).toBe(403);
|
|
945
|
+
expect(errorMessageOf(result)).toBe(INCIDENT_FORM_FOREIGN_PAGE_MESSAGE);
|
|
946
|
+
expect(result.headers["cache-control"]).toBe(
|
|
947
|
+
"no-store, no-cache, must-revalidate",
|
|
948
|
+
);
|
|
949
|
+
expect(getPublicForm).not.toHaveBeenCalled();
|
|
950
|
+
expect(client.counters.size).toBe(0);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
/*
|
|
954
|
+
* Six hundred and one images with made-up links, loaded by one
|
|
955
|
+
* visitor's browser in a loop: counted, they would have used up the 600
|
|
956
|
+
* reads a minute everybody behind that address shares, and a colleague
|
|
957
|
+
* opening a real form would be told to wait.
|
|
958
|
+
*/
|
|
959
|
+
it("cannot use up the read budget of the address it sends from", async () => {
|
|
960
|
+
for (let attempt: number = 0; attempt < 601; attempt++) {
|
|
961
|
+
const refused: HttpResult = await send({
|
|
962
|
+
port,
|
|
963
|
+
method: "GET",
|
|
964
|
+
path: readPath(ObjectID.generate().toString()),
|
|
965
|
+
clientIp: ALLOWED_IP,
|
|
966
|
+
withoutPageHeader: true,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
expect(refused.status).toBe(403);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// The page's own read, as a browser sends it over plain HTTP.
|
|
973
|
+
const colleague: HttpResult = await send({
|
|
974
|
+
port,
|
|
975
|
+
method: "GET",
|
|
976
|
+
path: readPath(SHARE_KEY),
|
|
977
|
+
clientIp: ALLOWED_IP,
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
expect(seen(colleague)).toEqual({ status: 200, body: PUBLIC_FORM });
|
|
981
|
+
|
|
982
|
+
// Only the colleague's read was counted: by address, and by form and address.
|
|
983
|
+
expect(
|
|
984
|
+
client
|
|
985
|
+
.keysMatching(`:read:i:${ALLOWED_IP}:`)
|
|
986
|
+
.map((key: string): number => {
|
|
987
|
+
return client.counters.get(key)!;
|
|
988
|
+
}),
|
|
989
|
+
).toEqual([1]);
|
|
990
|
+
expect(Array.from(client.counters.values())).toEqual([1, 1]);
|
|
991
|
+
}, 60_000);
|
|
992
|
+
|
|
993
|
+
/*
|
|
994
|
+
* What a browser really sends with the page's own read: a GET to its own
|
|
995
|
+
* origin carries no Origin, and over plain HTTP no Sec-Fetch-Site either
|
|
996
|
+
* - only the page's header says where it came from. Over HTTPS the
|
|
997
|
+
* browser adds Sec-Fetch-Site: same-origin.
|
|
998
|
+
*/
|
|
999
|
+
it.each([
|
|
1000
|
+
["over plain HTTP: the page's header and nothing else", {}],
|
|
1001
|
+
["over HTTPS", { "sec-fetch-site": "same-origin" }],
|
|
1002
|
+
])(
|
|
1003
|
+
"serves the form page's own read %s",
|
|
1004
|
+
async (_label: string, headers: http.OutgoingHttpHeaders) => {
|
|
1005
|
+
const result: HttpResult = await send({
|
|
1006
|
+
port,
|
|
1007
|
+
method: "GET",
|
|
1008
|
+
path: readPath(LOCKED_SHARE_KEY),
|
|
1009
|
+
clientIp: ALLOWED_IP,
|
|
1010
|
+
headers: headers,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
expect(seen(result)).toEqual({ status: 200, body: PUBLIC_FORM });
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
/*
|
|
1018
|
+
* Opening the read's own address in the address bar is a navigation:
|
|
1019
|
+
* the browser says the visitor made it (Sec-Fetch-Site: none), but it
|
|
1020
|
+
* carries no header of a page's, so it is not the form's page reading.
|
|
1021
|
+
*/
|
|
1022
|
+
it("refuses the read's address opened in the address bar", async () => {
|
|
1023
|
+
const result: HttpResult = await send({
|
|
1024
|
+
port,
|
|
1025
|
+
method: "GET",
|
|
1026
|
+
path: readPath(SHARE_KEY),
|
|
1027
|
+
headers: { "sec-fetch-site": "none" },
|
|
1028
|
+
withoutPageHeader: true,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
expect(result.status).toBe(403);
|
|
1032
|
+
expect(errorMessageOf(result)).toBe(INCIDENT_FORM_FOREIGN_PAGE_MESSAGE);
|
|
1033
|
+
expect(client.counters.size).toBe(0);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
/*
|
|
1037
|
+
* The page's header is asked of a read only. A submission cannot come
|
|
1038
|
+
* from another site's page unnoticed - every POST carries its Origin,
|
|
1039
|
+
* and a JSON body is preflighted - so a caller that is not a browser
|
|
1040
|
+
* submits without it.
|
|
1041
|
+
*/
|
|
1042
|
+
it("takes a submission without the page's header", async () => {
|
|
1043
|
+
const result: HttpResult = await send({
|
|
1044
|
+
port,
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
path: submitPath(SHARE_KEY),
|
|
1047
|
+
withoutPageHeader: true,
|
|
1048
|
+
body: { data: ANSWERS },
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
expect(seen(result)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("takes the JSON the page's API client sends, charset and all", async () => {
|
|
1055
|
+
const result: HttpResult = await send({
|
|
1056
|
+
port,
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
path: submitPath(SHARE_KEY),
|
|
1059
|
+
headers: {
|
|
1060
|
+
...OWN_PAGE,
|
|
1061
|
+
"content-type": "application/json;charset=UTF-8",
|
|
1062
|
+
},
|
|
1063
|
+
rawBody: JSON.stringify({ data: ANSWERS }),
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
expect(seen(result)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it.each([
|
|
1070
|
+
["from the form's own page", OWN_PAGE],
|
|
1071
|
+
["from a caller that is not a browser", {}],
|
|
1072
|
+
])(
|
|
1073
|
+
"refuses a submission that is not JSON %s, before anything counts it",
|
|
1074
|
+
async (_label: string, headers: http.OutgoingHttpHeaders) => {
|
|
1075
|
+
for (const contentType of [
|
|
1076
|
+
"application/x-www-form-urlencoded",
|
|
1077
|
+
"text/plain",
|
|
1078
|
+
]) {
|
|
1079
|
+
const result: HttpResult = await send({
|
|
1080
|
+
port,
|
|
1081
|
+
method: "POST",
|
|
1082
|
+
path: submitPath(SHARE_KEY),
|
|
1083
|
+
headers: { ...headers, "content-type": contentType },
|
|
1084
|
+
rawBody: FORGED_FORM,
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
expect(result.status).toBe(400);
|
|
1088
|
+
expect(errorMessageOf(result)).toBe(
|
|
1089
|
+
INCIDENT_FORM_SUBMISSION_BODY_MESSAGE,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
expect(submitPublicForm).not.toHaveBeenCalled();
|
|
1094
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
1095
|
+
expect(client.counters.size).toBe(0);
|
|
1096
|
+
},
|
|
1097
|
+
);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
describe("404", () => {
|
|
1101
|
+
it.each([
|
|
1102
|
+
["a link no form holds", UNKNOWN_SHARE_KEY],
|
|
1103
|
+
["a form that is turned off", OFF_SHARE_KEY],
|
|
1104
|
+
["a link that is not a share key", "report-a-problem"],
|
|
1105
|
+
])(
|
|
1106
|
+
"answers %s with the one not-available message",
|
|
1107
|
+
async (_label: string, shareKey: string) => {
|
|
1108
|
+
const read: HttpResult = await send({
|
|
1109
|
+
port,
|
|
1110
|
+
method: "GET",
|
|
1111
|
+
path: readPath(shareKey),
|
|
1112
|
+
});
|
|
1113
|
+
const submitted: HttpResult = await send({
|
|
1114
|
+
port,
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
path: submitPath(shareKey),
|
|
1117
|
+
body: { data: ANSWERS },
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
for (const result of [read, submitted]) {
|
|
1121
|
+
expect(result.status).toBe(404);
|
|
1122
|
+
expect(errorMessageOf(result)).toBe(
|
|
1123
|
+
INCIDENT_FORM_NOT_AVAILABLE_MESSAGE,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
1128
|
+
},
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
it("answers a project off plan exactly as a link no form holds", async () => {
|
|
1132
|
+
jest
|
|
1133
|
+
.spyOn(IncidentFormService, "isProjectOnPlan")
|
|
1134
|
+
.mockResolvedValue(false as never);
|
|
1135
|
+
|
|
1136
|
+
const offPlan: HttpResult = await send({
|
|
1137
|
+
port,
|
|
1138
|
+
method: "GET",
|
|
1139
|
+
path: readPath(SHARE_KEY),
|
|
1140
|
+
});
|
|
1141
|
+
const unknown: HttpResult = await send({
|
|
1142
|
+
port,
|
|
1143
|
+
method: "GET",
|
|
1144
|
+
path: readPath(UNKNOWN_SHARE_KEY),
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
expect(seen(offPlan)).toEqual(seen(unknown));
|
|
1148
|
+
expect(offPlan.status).toBe(404);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
describe("429", () => {
|
|
1153
|
+
it("refuses a caller over the read limit, saying when to come back", async () => {
|
|
1154
|
+
client.countFloor = 10_000;
|
|
1155
|
+
|
|
1156
|
+
const result: HttpResult = await send({
|
|
1157
|
+
port,
|
|
1158
|
+
method: "GET",
|
|
1159
|
+
path: readPath(SHARE_KEY),
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
expect(result.status).toBe(429);
|
|
1163
|
+
expect(errorMessageOf(result)).toBe(
|
|
1164
|
+
INCIDENT_FORM_READ_RATE_LIMIT_MESSAGE,
|
|
1165
|
+
);
|
|
1166
|
+
expect(Number(result.headers["retry-after"])).toBeGreaterThan(0);
|
|
1167
|
+
expect(getPublicForm).not.toHaveBeenCalled();
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it("refuses a caller over the submission limit, declaring nothing", async () => {
|
|
1171
|
+
for (let i: number = 0; i < 10; i++) {
|
|
1172
|
+
expect(
|
|
1173
|
+
(
|
|
1174
|
+
await send({
|
|
1175
|
+
port,
|
|
1176
|
+
method: "POST",
|
|
1177
|
+
path: submitPath(SHARE_KEY),
|
|
1178
|
+
body: { data: ANSWERS },
|
|
1179
|
+
})
|
|
1180
|
+
).status,
|
|
1181
|
+
).toBe(200);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const result: HttpResult = await send({
|
|
1185
|
+
port,
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
path: submitPath(SHARE_KEY),
|
|
1188
|
+
body: { data: ANSWERS },
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
expect(result.status).toBe(429);
|
|
1192
|
+
expect(errorMessageOf(result)).toBe(
|
|
1193
|
+
INCIDENT_FORM_SUBMIT_RATE_LIMIT_MESSAGE,
|
|
1194
|
+
);
|
|
1195
|
+
expect(Number(result.headers["retry-after"])).toBeGreaterThan(0);
|
|
1196
|
+
expect(incidentCreate).toHaveBeenCalledTimes(10);
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
/*
|
|
1201
|
+
* The form's hourly ceiling - sixty incidents across every address -
|
|
1202
|
+
* bounds the pages a leaked link can cause. Only a submission that passed
|
|
1203
|
+
* every check spends it: if refused requests did, anyone holding the link
|
|
1204
|
+
* - even from outside the IP allowlist, or without solving the captcha -
|
|
1205
|
+
* could use it up with requests that declare nothing, and lock the form
|
|
1206
|
+
* for everybody, silently, hour after hour.
|
|
1207
|
+
*/
|
|
1208
|
+
describe("the form's hourly ceiling", () => {
|
|
1209
|
+
// Ten requests from each of six addresses: within each one's own budget.
|
|
1210
|
+
async function sendFromSixAddresses(data: {
|
|
1211
|
+
firstOctet: number;
|
|
1212
|
+
shareKey: string;
|
|
1213
|
+
body: unknown;
|
|
1214
|
+
}): Promise<Array<number>> {
|
|
1215
|
+
const statuses: Array<number> = [];
|
|
1216
|
+
|
|
1217
|
+
for (let address: number = 1; address <= 6; address++) {
|
|
1218
|
+
for (let attempt: number = 0; attempt < 10; attempt++) {
|
|
1219
|
+
statuses.push(
|
|
1220
|
+
(
|
|
1221
|
+
await send({
|
|
1222
|
+
port,
|
|
1223
|
+
method: "POST",
|
|
1224
|
+
path: submitPath(data.shareKey),
|
|
1225
|
+
clientIp: `${data.firstOctet}.0.0.${address}`,
|
|
1226
|
+
body: data.body,
|
|
1227
|
+
})
|
|
1228
|
+
).status,
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return statuses;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
test("is never spent by requests the IP allowlist, the captcha or the answers refuse", async () => {
|
|
1237
|
+
// Outside the form's allowlist (198.51.100.0/24).
|
|
1238
|
+
expect(
|
|
1239
|
+
new Set(
|
|
1240
|
+
await sendFromSixAddresses({
|
|
1241
|
+
firstOctet: 11,
|
|
1242
|
+
shareKey: LOCKED_SHARE_KEY,
|
|
1243
|
+
body: { data: ANSWERS },
|
|
1244
|
+
}),
|
|
1245
|
+
),
|
|
1246
|
+
).toEqual(new Set([403]));
|
|
1247
|
+
|
|
1248
|
+
// A captcha that fails.
|
|
1249
|
+
jest.spyOn(CaptchaUtil, "isCaptchaEnabled").mockReturnValue(true);
|
|
1250
|
+
jest
|
|
1251
|
+
.spyOn(CaptchaUtil, "verifyCaptcha")
|
|
1252
|
+
.mockRejectedValue(
|
|
1253
|
+
new BadDataException(
|
|
1254
|
+
"Captcha verification failed. Please try again.",
|
|
1255
|
+
),
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
expect(
|
|
1259
|
+
new Set(
|
|
1260
|
+
await sendFromSixAddresses({
|
|
1261
|
+
firstOctet: 12,
|
|
1262
|
+
shareKey: LOCKED_SHARE_KEY,
|
|
1263
|
+
body: { data: ANSWERS },
|
|
1264
|
+
}),
|
|
1265
|
+
),
|
|
1266
|
+
).toEqual(new Set([403]));
|
|
1267
|
+
expect(
|
|
1268
|
+
new Set(
|
|
1269
|
+
await sendFromSixAddresses({
|
|
1270
|
+
firstOctet: 12,
|
|
1271
|
+
shareKey: SHARE_KEY,
|
|
1272
|
+
body: { data: ANSWERS },
|
|
1273
|
+
}),
|
|
1274
|
+
),
|
|
1275
|
+
).toEqual(new Set([400]));
|
|
1276
|
+
|
|
1277
|
+
jest.spyOn(CaptchaUtil, "isCaptchaEnabled").mockReturnValue(false);
|
|
1278
|
+
|
|
1279
|
+
// Bodies that are not a submission, and answers that do not pass.
|
|
1280
|
+
expect(
|
|
1281
|
+
new Set(
|
|
1282
|
+
await sendFromSixAddresses({
|
|
1283
|
+
firstOctet: 13,
|
|
1284
|
+
shareKey: SHARE_KEY,
|
|
1285
|
+
body: {},
|
|
1286
|
+
}),
|
|
1287
|
+
),
|
|
1288
|
+
).toEqual(new Set([400]));
|
|
1289
|
+
expect(
|
|
1290
|
+
new Set(
|
|
1291
|
+
await sendFromSixAddresses({
|
|
1292
|
+
firstOctet: 14,
|
|
1293
|
+
shareKey: SHARE_KEY,
|
|
1294
|
+
body: { data: { reporterName: "No title" } },
|
|
1295
|
+
}),
|
|
1296
|
+
),
|
|
1297
|
+
).toEqual(new Set([400]));
|
|
1298
|
+
|
|
1299
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
1300
|
+
expect(client.keysMatching(":submit:f:")).toEqual([]);
|
|
1301
|
+
|
|
1302
|
+
// The office inside the allowlist can still report.
|
|
1303
|
+
const insider: HttpResult = await send({
|
|
1304
|
+
port,
|
|
1305
|
+
method: "POST",
|
|
1306
|
+
path: submitPath(LOCKED_SHARE_KEY),
|
|
1307
|
+
clientIp: ALLOWED_IP,
|
|
1308
|
+
body: { data: ANSWERS },
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
expect(seen(insider)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
1312
|
+
expect(incidentCreate).toHaveBeenCalledTimes(1);
|
|
1313
|
+
expect(client.keysMatching(":submit:f:")).toHaveLength(1);
|
|
1314
|
+
}, 60_000);
|
|
1315
|
+
|
|
1316
|
+
test("declares at most sixty incidents an hour, from however many addresses", async () => {
|
|
1317
|
+
for (let address: number = 1; address <= 60; address++) {
|
|
1318
|
+
expect(
|
|
1319
|
+
(
|
|
1320
|
+
await send({
|
|
1321
|
+
port,
|
|
1322
|
+
method: "POST",
|
|
1323
|
+
path: submitPath(SHARE_KEY),
|
|
1324
|
+
clientIp: `10.3.0.${address}`,
|
|
1325
|
+
body: { data: ANSWERS },
|
|
1326
|
+
})
|
|
1327
|
+
).status,
|
|
1328
|
+
).toBe(200);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const refused: HttpResult = await send({
|
|
1332
|
+
port,
|
|
1333
|
+
method: "POST",
|
|
1334
|
+
path: submitPath(SHARE_KEY),
|
|
1335
|
+
clientIp: "10.3.1.1",
|
|
1336
|
+
body: { data: ANSWERS },
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
expect(refused.status).toBe(429);
|
|
1340
|
+
expect(errorMessageOf(refused)).toBe(
|
|
1341
|
+
INCIDENT_FORM_TOTAL_RATE_LIMIT_MESSAGE,
|
|
1342
|
+
);
|
|
1343
|
+
expect(Number(refused.headers["retry-after"])).toBeGreaterThan(0);
|
|
1344
|
+
expect(Number(refused.headers["retry-after"])).toBeLessThanOrEqual(
|
|
1345
|
+
60 * 60,
|
|
1346
|
+
);
|
|
1347
|
+
expect(incidentCreate).toHaveBeenCalledTimes(60);
|
|
1348
|
+
}, 60_000);
|
|
1349
|
+
|
|
1350
|
+
test("refuses with a 503, declaring nothing, when it cannot be counted", async () => {
|
|
1351
|
+
client.failKeysMatching = /:submit:f:/;
|
|
1352
|
+
|
|
1353
|
+
const result: HttpResult = await send({
|
|
1354
|
+
port,
|
|
1355
|
+
method: "POST",
|
|
1356
|
+
path: submitPath(SHARE_KEY),
|
|
1357
|
+
body: { data: ANSWERS },
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
expect(result.status).toBe(503);
|
|
1361
|
+
expect(errorMessageOf(result)).toBe(
|
|
1362
|
+
INCIDENT_FORM_RATE_LIMIT_UNAVAILABLE_MESSAGE,
|
|
1363
|
+
);
|
|
1364
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
describe("503", () => {
|
|
1369
|
+
it("refuses submissions while the rate limit counter is unavailable", async () => {
|
|
1370
|
+
isConnectedMock.mockReturnValue(false);
|
|
1371
|
+
|
|
1372
|
+
const result: HttpResult = await send({
|
|
1373
|
+
port,
|
|
1374
|
+
method: "POST",
|
|
1375
|
+
path: submitPath(SHARE_KEY),
|
|
1376
|
+
body: { data: ANSWERS },
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
expect(result.status).toBe(503);
|
|
1380
|
+
expect(errorMessageOf(result)).toBe(
|
|
1381
|
+
INCIDENT_FORM_RATE_LIMIT_UNAVAILABLE_MESSAGE,
|
|
1382
|
+
);
|
|
1383
|
+
expect(submitPublicForm).not.toHaveBeenCalled();
|
|
1384
|
+
expect(incidentCreate).not.toHaveBeenCalled();
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it("still shows the form while the counter is unavailable", async () => {
|
|
1388
|
+
isConnectedMock.mockReturnValue(false);
|
|
1389
|
+
|
|
1390
|
+
const result: HttpResult = await send({
|
|
1391
|
+
port,
|
|
1392
|
+
method: "GET",
|
|
1393
|
+
path: readPath(SHARE_KEY),
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
expect(seen(result)).toEqual({ status: 200, body: PUBLIC_FORM });
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
describe("a visitor's own OneUptime session never gets in the way", () => {
|
|
1401
|
+
const deadSessions: Array<
|
|
1402
|
+
[string, Dictionary<string>, http.OutgoingHttpHeaders]
|
|
1403
|
+
> = [
|
|
1404
|
+
[
|
|
1405
|
+
"an access-token cookie from before a secret rotation",
|
|
1406
|
+
{ [CookieUtil.getUserTokenKey()]: STALE_ACCESS_TOKEN },
|
|
1407
|
+
{},
|
|
1408
|
+
],
|
|
1409
|
+
[
|
|
1410
|
+
"an expired access-token cookie",
|
|
1411
|
+
{ [CookieUtil.getUserTokenKey()]: EXPIRED_ACCESS_TOKEN },
|
|
1412
|
+
{},
|
|
1413
|
+
],
|
|
1414
|
+
[
|
|
1415
|
+
"a junk access-token cookie",
|
|
1416
|
+
{ [CookieUtil.getUserTokenKey()]: "not-a-jwt" },
|
|
1417
|
+
{},
|
|
1418
|
+
],
|
|
1419
|
+
["a junk bearer token", {}, { authorization: "Bearer not-a-jwt" }],
|
|
1420
|
+
];
|
|
1421
|
+
|
|
1422
|
+
it.each(deadSessions)(
|
|
1423
|
+
"answers a request carrying %s exactly as one carrying none",
|
|
1424
|
+
async (
|
|
1425
|
+
_label: string,
|
|
1426
|
+
cookies: Dictionary<string>,
|
|
1427
|
+
headers: http.OutgoingHttpHeaders,
|
|
1428
|
+
) => {
|
|
1429
|
+
for (const [method, path, body] of [
|
|
1430
|
+
["GET", readPath(SHARE_KEY), undefined],
|
|
1431
|
+
["POST", submitPath(SHARE_KEY), { data: ANSWERS }],
|
|
1432
|
+
["GET", readPath(OFF_SHARE_KEY), undefined],
|
|
1433
|
+
["POST", submitPath(SHARE_KEY), { data: { title: "" } }],
|
|
1434
|
+
] as Array<["GET" | "POST", string, unknown]>) {
|
|
1435
|
+
const withSession: HttpResult = await send({
|
|
1436
|
+
port,
|
|
1437
|
+
method,
|
|
1438
|
+
path,
|
|
1439
|
+
cookies,
|
|
1440
|
+
headers,
|
|
1441
|
+
body,
|
|
1442
|
+
});
|
|
1443
|
+
const withoutSession: HttpResult = await send({
|
|
1444
|
+
port,
|
|
1445
|
+
method,
|
|
1446
|
+
path,
|
|
1447
|
+
body,
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
expect(withSession.status).not.toBe(401);
|
|
1451
|
+
expect(seen(withSession)).toEqual(seen(withoutSession));
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
/*
|
|
1457
|
+
* Nothing on these routes touches the visitor's cookies, so nothing can
|
|
1458
|
+
* sign them out of the session they have on this host.
|
|
1459
|
+
*/
|
|
1460
|
+
it.each(deadSessions)(
|
|
1461
|
+
"never sets or clears a cookie for a request carrying %s",
|
|
1462
|
+
async (
|
|
1463
|
+
_label: string,
|
|
1464
|
+
cookies: Dictionary<string>,
|
|
1465
|
+
headers: http.OutgoingHttpHeaders,
|
|
1466
|
+
) => {
|
|
1467
|
+
const read: HttpResult = await send({
|
|
1468
|
+
port,
|
|
1469
|
+
method: "GET",
|
|
1470
|
+
path: readPath(SHARE_KEY),
|
|
1471
|
+
cookies,
|
|
1472
|
+
headers,
|
|
1473
|
+
});
|
|
1474
|
+
const submitted: HttpResult = await send({
|
|
1475
|
+
port,
|
|
1476
|
+
method: "POST",
|
|
1477
|
+
path: submitPath(SHARE_KEY),
|
|
1478
|
+
cookies,
|
|
1479
|
+
headers,
|
|
1480
|
+
body: { data: ANSWERS },
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
expect(read.headers["set-cookie"]).toBeUndefined();
|
|
1484
|
+
expect(submitted.headers["set-cookie"]).toBeUndefined();
|
|
1485
|
+
},
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
it("treats a signed-in visitor as just a visitor: the same answer, and an incident that names no user", async () => {
|
|
1489
|
+
const cookies: Dictionary<string> = {
|
|
1490
|
+
[CookieUtil.getUserTokenKey()]: validAccessToken(),
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const read: HttpResult = await send({
|
|
1494
|
+
port,
|
|
1495
|
+
method: "GET",
|
|
1496
|
+
path: readPath(SHARE_KEY),
|
|
1497
|
+
cookies,
|
|
1498
|
+
});
|
|
1499
|
+
const submitted: HttpResult = await send({
|
|
1500
|
+
port,
|
|
1501
|
+
method: "POST",
|
|
1502
|
+
path: submitPath(SHARE_KEY),
|
|
1503
|
+
cookies,
|
|
1504
|
+
body: { data: ANSWERS },
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
expect(seen(read)).toEqual({ status: 200, body: PUBLIC_FORM });
|
|
1508
|
+
expect(seen(submitted)).toEqual({ status: 200, body: SUBMISSION_RESULT });
|
|
1509
|
+
expect(submitted.headers["set-cookie"]).toBeUndefined();
|
|
1510
|
+
|
|
1511
|
+
const created: { data: Incident; props: JSONObject } = incidentCreate.mock
|
|
1512
|
+
.calls[0]![0] as { data: Incident; props: JSONObject };
|
|
1513
|
+
|
|
1514
|
+
expect(created.data.createdByUserId).toBeUndefined();
|
|
1515
|
+
expect(created.props).toEqual({ isRoot: true });
|
|
1516
|
+
expect(
|
|
1517
|
+
Object.keys(
|
|
1518
|
+
getPublicForm.mock.calls[0]![0] as Record<string, unknown>,
|
|
1519
|
+
).sort(),
|
|
1520
|
+
).toEqual(["clientIp", "shareKey"]);
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
/*
|
|
1524
|
+
* The CRUD routes on the same router do need to know who is calling, so
|
|
1525
|
+
* a dead session is still a 401 there - the answer that makes the
|
|
1526
|
+
* dashboard client refresh its session. Only the public routes waive it.
|
|
1527
|
+
*/
|
|
1528
|
+
it("still answers the signed-in CRUD routes beside them with a 401", async () => {
|
|
1529
|
+
for (const [method, path] of [
|
|
1530
|
+
["POST", "/api/incident-form/get-list"],
|
|
1531
|
+
[
|
|
1532
|
+
"POST",
|
|
1533
|
+
`/api/incident-form/${ObjectID.generate().toString()}/get-item`,
|
|
1534
|
+
],
|
|
1535
|
+
] as Array<["GET" | "POST", string]>) {
|
|
1536
|
+
const result: HttpResult = await send({
|
|
1537
|
+
port,
|
|
1538
|
+
method,
|
|
1539
|
+
path,
|
|
1540
|
+
cookies: { [CookieUtil.getUserTokenKey()]: STALE_ACCESS_TOKEN },
|
|
1541
|
+
body: {},
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
expect(result.status).toBe(401);
|
|
1545
|
+
expect(errorMessageOf(result)).toBe(INVALID_ACCESS_TOKEN_MESSAGE);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
/*
|
|
1552
|
+
* Route matching, read from the router itself. The public routes sit beside
|
|
1553
|
+
* the model's CRUD routes under /incident-form; neither may ever catch the
|
|
1554
|
+
* other's requests. Express matches in registration order, and BaseAPI
|
|
1555
|
+
* registers the CRUD routes first.
|
|
1556
|
+
*/
|
|
1557
|
+
describe("the public routes and the CRUD routes never collide", () => {
|
|
1558
|
+
type RouteHandler = (...args: Array<unknown>) => unknown;
|
|
1559
|
+
|
|
1560
|
+
interface RouterLayer {
|
|
1561
|
+
route?: {
|
|
1562
|
+
path: string;
|
|
1563
|
+
methods: Dictionary<boolean>;
|
|
1564
|
+
stack: Array<{ handle: RouteHandler }>;
|
|
1565
|
+
};
|
|
1566
|
+
match: (path: string) => boolean;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const router: ExpressRouter = new IncidentFormAPI().getRouter();
|
|
1570
|
+
const layers: Array<RouterLayer> = (
|
|
1571
|
+
router as unknown as { stack: Array<RouterLayer> }
|
|
1572
|
+
).stack.filter((layer: RouterLayer) => {
|
|
1573
|
+
return Boolean(layer.route);
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
// Every registered route that would answer this request, in order.
|
|
1577
|
+
function routesFor(method: string, path: string): Array<string> {
|
|
1578
|
+
return layers
|
|
1579
|
+
.filter((layer: RouterLayer) => {
|
|
1580
|
+
return (
|
|
1581
|
+
Boolean(layer.route!.methods[method.toLowerCase()]) &&
|
|
1582
|
+
layer.match(path)
|
|
1583
|
+
);
|
|
1584
|
+
})
|
|
1585
|
+
.map((layer: RouterLayer) => {
|
|
1586
|
+
return `${method} ${layer.route!.path}`;
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function firstMiddlewareFor(method: string, path: string): unknown {
|
|
1591
|
+
const layer: RouterLayer | undefined = layers.find(
|
|
1592
|
+
(candidate: RouterLayer) => {
|
|
1593
|
+
return (
|
|
1594
|
+
Boolean(candidate.route!.methods[method.toLowerCase()]) &&
|
|
1595
|
+
candidate.match(path)
|
|
1596
|
+
);
|
|
1597
|
+
},
|
|
1598
|
+
);
|
|
1599
|
+
|
|
1600
|
+
return layer?.route?.stack[0]?.handle;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const shareKey: string = ObjectID.generate().toString();
|
|
1604
|
+
const id: string = ObjectID.generate().toString();
|
|
1605
|
+
|
|
1606
|
+
it("sends a form link only to the public routes", () => {
|
|
1607
|
+
expect(routesFor("GET", `/incident-form/public/${shareKey}`)).toEqual([
|
|
1608
|
+
"GET /incident-form/public/:shareKey",
|
|
1609
|
+
]);
|
|
1610
|
+
expect(
|
|
1611
|
+
routesFor("POST", `/incident-form/public/${shareKey}/submit`),
|
|
1612
|
+
).toEqual(["POST /incident-form/public/:shareKey/submit"]);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
it.each([
|
|
1616
|
+
["POST", "/incident-form"],
|
|
1617
|
+
["POST", "/incident-form/get-list"],
|
|
1618
|
+
["GET", "/incident-form/get-list"],
|
|
1619
|
+
["POST", "/incident-form/count"],
|
|
1620
|
+
["POST", `/incident-form/${id}/get-item`],
|
|
1621
|
+
["GET", `/incident-form/${id}/get-item`],
|
|
1622
|
+
["PUT", `/incident-form/${id}`],
|
|
1623
|
+
["POST", `/incident-form/${id}/update-item`],
|
|
1624
|
+
["DELETE", `/incident-form/${id}`],
|
|
1625
|
+
["POST", `/incident-form/${id}/delete-item`],
|
|
1626
|
+
])(
|
|
1627
|
+
"sends %s %s only to its CRUD route, behind the signed-in user middleware",
|
|
1628
|
+
(method: string, path: string) => {
|
|
1629
|
+
const matched: Array<string> = routesFor(method, path);
|
|
1630
|
+
|
|
1631
|
+
expect(matched).toHaveLength(1);
|
|
1632
|
+
expect(matched[0]).not.toContain("/public/");
|
|
1633
|
+
expect(firstMiddlewareFor(method, path)).toBe(
|
|
1634
|
+
UserMiddleware.getUserMiddleware,
|
|
1635
|
+
);
|
|
1636
|
+
},
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1639
|
+
/*
|
|
1640
|
+
* A share key is a UUID, so it is never "get-item". Were one spelled like
|
|
1641
|
+
* a CRUD suffix, the CRUD route - registered first - would take it, with
|
|
1642
|
+
* its authentication: never the other way round.
|
|
1643
|
+
*/
|
|
1644
|
+
it("lets the CRUD route win the one path both could match", () => {
|
|
1645
|
+
expect(routesFor("GET", "/incident-form/public/get-item")).toEqual([
|
|
1646
|
+
"GET /incident-form/:id/get-item",
|
|
1647
|
+
"GET /incident-form/public/:shareKey",
|
|
1648
|
+
]);
|
|
1649
|
+
expect(firstMiddlewareFor("GET", "/incident-form/public/get-item")).toBe(
|
|
1650
|
+
UserMiddleware.getUserMiddleware,
|
|
1651
|
+
);
|
|
1652
|
+
});
|
|
1653
|
+
});
|