@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,1916 @@
|
|
|
1
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
2
|
+
import IncidentCustomField from "../../../Models/DatabaseModels/IncidentCustomField";
|
|
3
|
+
import { getIncidentFormAskedDefinitions } from "../../../Types/CustomField/CustomFieldCreateSettings";
|
|
4
|
+
import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_INCIDENT_FORM_DESCRIPTION_SETTING,
|
|
7
|
+
INCIDENT_FORM_CUSTOM_FIELD_TEXT_MAX_LENGTH,
|
|
8
|
+
INCIDENT_FORM_DESCRIPTION_MAX_LENGTH,
|
|
9
|
+
INCIDENT_FORM_FIELD_SETTINGS,
|
|
10
|
+
INCIDENT_FORM_MULTI_SELECT_MAX_CHOICES,
|
|
11
|
+
INCIDENT_FORM_QUESTION_LABELS,
|
|
12
|
+
INCIDENT_FORM_REPORTER_EMAIL_MAX_LENGTH,
|
|
13
|
+
INCIDENT_FORM_REPORTER_NAME_MAX_LENGTH,
|
|
14
|
+
INCIDENT_FORM_TITLE_MAX_LENGTH,
|
|
15
|
+
IncidentFormAskedDefinition,
|
|
16
|
+
IncidentFormFieldSetting,
|
|
17
|
+
IncidentFormSubmissionRules,
|
|
18
|
+
IncidentFormSubmissionValidationResult,
|
|
19
|
+
PublicIncidentForm,
|
|
20
|
+
PublicIncidentFormField,
|
|
21
|
+
PublicIncidentFormSeverity,
|
|
22
|
+
PublicIncidentFormSource,
|
|
23
|
+
ValidatedIncidentFormSubmission,
|
|
24
|
+
formatIncidentFormSubmissionErrors,
|
|
25
|
+
getPublicIncidentForm,
|
|
26
|
+
getPublicIncidentFormFields,
|
|
27
|
+
isIncidentFormFieldSetting,
|
|
28
|
+
validateIncidentFormSubmission,
|
|
29
|
+
} from "../../../Types/Incident/IncidentFormPublic";
|
|
30
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
31
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
32
|
+
import { CUSTOM_FIELD_MUST_BE_CHECKED_MESSAGE } from "../../../UI/Components/CustomFields/CustomFieldFormFields";
|
|
33
|
+
import { describe, expect, test } from "@jest/globals";
|
|
34
|
+
import fs from "fs";
|
|
35
|
+
import path from "path";
|
|
36
|
+
import { getMetadataArgsStorage } from "typeorm";
|
|
37
|
+
import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* The public half of incident forms: what a stranger's browser is told about
|
|
41
|
+
* a form, and the rules a stranger's submission is held to before an
|
|
42
|
+
* incident is declared from it.
|
|
43
|
+
*
|
|
44
|
+
* Everything here is a boundary the server relies on. The incident is built
|
|
45
|
+
* only from validateIncidentFormSubmission's value, so what matters most is
|
|
46
|
+
* what can NOT get through it: answers to fields the form does not ask,
|
|
47
|
+
* anything that is not an answer (a projectId, a state, isPrivate), text
|
|
48
|
+
* longer than its column, and a severity the form does not offer.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const SEVERITY_HIGH: string = "0a0a0a0a-0000-4000-8000-000000000001";
|
|
52
|
+
const SEVERITY_LOW: string = "0a0a0a0a-0000-4000-8000-000000000002";
|
|
53
|
+
|
|
54
|
+
const SEVERITIES: Array<PublicIncidentFormSeverity> = [
|
|
55
|
+
{ _id: SEVERITY_HIGH, name: "High", color: "#ff0000" },
|
|
56
|
+
{ _id: SEVERITY_LOW, name: "Low" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// The rules of a form that asks for nothing optional beyond the defaults.
|
|
60
|
+
const DEFAULT_RULES: IncidentFormSubmissionRules = {
|
|
61
|
+
descriptionSetting: IncidentFormFieldSetting.Optional,
|
|
62
|
+
isReporterDetailsRequired: true,
|
|
63
|
+
allowReporterToChooseSeverity: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// The fields a form might ask for, as getIncidentFormAskedDefinitions hands them.
|
|
67
|
+
const ASKED: Array<IncidentFormAskedDefinition> = [
|
|
68
|
+
{
|
|
69
|
+
name: "Impact",
|
|
70
|
+
customFieldType: CustomFieldType.Text,
|
|
71
|
+
isRequiredOnCreate: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Steps to Reproduce",
|
|
75
|
+
customFieldType: CustomFieldType.LongText,
|
|
76
|
+
isRequiredOnCreate: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "Notes",
|
|
80
|
+
customFieldType: CustomFieldType.Markdown,
|
|
81
|
+
isRequiredOnCreate: false,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "Users Affected",
|
|
85
|
+
customFieldType: CustomFieldType.Number,
|
|
86
|
+
isRequiredOnCreate: false,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "I Have Checked The Status Page",
|
|
90
|
+
customFieldType: CustomFieldType.Boolean,
|
|
91
|
+
isRequiredOnCreate: true,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "Customer Facing",
|
|
95
|
+
customFieldType: CustomFieldType.Boolean,
|
|
96
|
+
isRequiredOnCreate: false,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "Region",
|
|
100
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
101
|
+
dropdownOptions: "EU\nUS",
|
|
102
|
+
isRequiredOnCreate: false,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "Systems",
|
|
106
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
107
|
+
dropdownOptions: '[{"value":"API","color":"#112233"},{"value":"Web"}]',
|
|
108
|
+
isRequiredOnCreate: false,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "Noticed On",
|
|
112
|
+
customFieldType: CustomFieldType.Date,
|
|
113
|
+
isRequiredOnCreate: false,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "Noticed At",
|
|
117
|
+
customFieldType: CustomFieldType.DateTime,
|
|
118
|
+
isRequiredOnCreate: false,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "Untyped",
|
|
122
|
+
customFieldType: undefined,
|
|
123
|
+
isRequiredOnCreate: false,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// The answers every required question needs.
|
|
128
|
+
const REQUIRED_ANSWERS: JSONObject = {
|
|
129
|
+
Impact: "Checkout is down",
|
|
130
|
+
"I Have Checked The Status Page": true,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type SubmissionFunction = (overrides?: JSONObject) => JSONObject;
|
|
134
|
+
|
|
135
|
+
const submission: SubmissionFunction = (
|
|
136
|
+
overrides: JSONObject = {},
|
|
137
|
+
): JSONObject => {
|
|
138
|
+
return {
|
|
139
|
+
title: "Checkout fails",
|
|
140
|
+
reporterName: "Jane Doe",
|
|
141
|
+
reporterEmail: "jane@example.com",
|
|
142
|
+
customFields: { ...REQUIRED_ANSWERS },
|
|
143
|
+
...overrides,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type ValidateFunction = (
|
|
148
|
+
data: unknown,
|
|
149
|
+
options?: {
|
|
150
|
+
form?: IncidentFormSubmissionRules;
|
|
151
|
+
askedDefinitions?: Array<IncidentFormAskedDefinition>;
|
|
152
|
+
severities?: Array<{ _id: string }>;
|
|
153
|
+
},
|
|
154
|
+
) => IncidentFormSubmissionValidationResult;
|
|
155
|
+
|
|
156
|
+
const validate: ValidateFunction = (
|
|
157
|
+
data: unknown,
|
|
158
|
+
options: {
|
|
159
|
+
form?: IncidentFormSubmissionRules;
|
|
160
|
+
askedDefinitions?: Array<IncidentFormAskedDefinition>;
|
|
161
|
+
severities?: Array<{ _id: string }>;
|
|
162
|
+
} = {},
|
|
163
|
+
): IncidentFormSubmissionValidationResult => {
|
|
164
|
+
return validateIncidentFormSubmission({
|
|
165
|
+
form: options.form || DEFAULT_RULES,
|
|
166
|
+
askedDefinitions: options.askedDefinitions || ASKED,
|
|
167
|
+
severities: options.severities || SEVERITIES,
|
|
168
|
+
data: data,
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type ErrorsOfFunction = (
|
|
173
|
+
result: IncidentFormSubmissionValidationResult,
|
|
174
|
+
) => Array<string>;
|
|
175
|
+
|
|
176
|
+
const errorsOf: ErrorsOfFunction = (
|
|
177
|
+
result: IncidentFormSubmissionValidationResult,
|
|
178
|
+
): Array<string> => {
|
|
179
|
+
if (result.isValid) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Expected the submission to be refused, but it passed: ${JSON.stringify(
|
|
182
|
+
result.value,
|
|
183
|
+
)}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result.errors;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
type ValueOfFunction = (
|
|
191
|
+
result: IncidentFormSubmissionValidationResult,
|
|
192
|
+
) => ValidatedIncidentFormSubmission;
|
|
193
|
+
|
|
194
|
+
const valueOf: ValueOfFunction = (
|
|
195
|
+
result: IncidentFormSubmissionValidationResult,
|
|
196
|
+
): ValidatedIncidentFormSubmission => {
|
|
197
|
+
if (!result.isValid) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Expected the submission to pass, but it was refused: ${result.errors.join(
|
|
200
|
+
" | ",
|
|
201
|
+
)}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result.value;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
type ColumnLengthFunction = (
|
|
209
|
+
target: unknown,
|
|
210
|
+
propertyName: string,
|
|
211
|
+
) => number | undefined;
|
|
212
|
+
|
|
213
|
+
// The length TypeORM creates a varchar column with.
|
|
214
|
+
const columnLength: ColumnLengthFunction = (
|
|
215
|
+
target: unknown,
|
|
216
|
+
propertyName: string,
|
|
217
|
+
): number | undefined => {
|
|
218
|
+
const column: ColumnMetadataArgs | undefined =
|
|
219
|
+
getMetadataArgsStorage().columns.find(
|
|
220
|
+
(candidate: ColumnMetadataArgs): boolean => {
|
|
221
|
+
return (
|
|
222
|
+
candidate.target === target && candidate.propertyName === propertyName
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const length: string | number | undefined = column?.options.length;
|
|
228
|
+
|
|
229
|
+
return length === undefined ? undefined : Number(length);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
type DeepFreezeFunction = <T>(value: T) => T;
|
|
233
|
+
|
|
234
|
+
const deepFreeze: DeepFreezeFunction = <T>(value: T): T => {
|
|
235
|
+
if (value && typeof value === "object") {
|
|
236
|
+
for (const child of Object.values(
|
|
237
|
+
value as unknown as Record<string, unknown>,
|
|
238
|
+
)) {
|
|
239
|
+
deepFreeze(child);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Object.freeze(value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return value;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
describe("IncidentFormFieldSetting", () => {
|
|
249
|
+
test("stores the three settings under these exact strings", () => {
|
|
250
|
+
expect(IncidentFormFieldSetting.Required).toBe("Required");
|
|
251
|
+
expect(IncidentFormFieldSetting.Optional).toBe("Optional");
|
|
252
|
+
expect(IncidentFormFieldSetting.Hidden).toBe("Hidden");
|
|
253
|
+
expect(INCIDENT_FORM_FIELD_SETTINGS).toEqual([
|
|
254
|
+
IncidentFormFieldSetting.Required,
|
|
255
|
+
IncidentFormFieldSetting.Optional,
|
|
256
|
+
IncidentFormFieldSetting.Hidden,
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/*
|
|
261
|
+
* IncidentFormModel.test.ts pins that the IncidentForm column defaults to
|
|
262
|
+
* the same value.
|
|
263
|
+
*/
|
|
264
|
+
test("a form asks for an optional description unless told otherwise", () => {
|
|
265
|
+
expect(DEFAULT_INCIDENT_FORM_DESCRIPTION_SETTING).toBe(
|
|
266
|
+
IncidentFormFieldSetting.Optional,
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test.each(["Required", "Optional", "Hidden"])(
|
|
271
|
+
"%s is a setting",
|
|
272
|
+
(value: string) => {
|
|
273
|
+
expect(isIncidentFormFieldSetting(value)).toBe(true);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
test.each([
|
|
278
|
+
["Default"],
|
|
279
|
+
["required"],
|
|
280
|
+
["OPTIONAL"],
|
|
281
|
+
[" Hidden"],
|
|
282
|
+
[""],
|
|
283
|
+
[null],
|
|
284
|
+
[undefined],
|
|
285
|
+
[1],
|
|
286
|
+
[{}],
|
|
287
|
+
])("%j is not a setting", (value: unknown) => {
|
|
288
|
+
expect(isIncidentFormFieldSetting(value)).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("the size caps fit the columns the answers land in", () => {
|
|
293
|
+
test("a title is at most as long as Incident.title", () => {
|
|
294
|
+
expect(INCIDENT_FORM_TITLE_MAX_LENGTH).toBe(500);
|
|
295
|
+
expect(columnLength(Incident, "title")).toBe(
|
|
296
|
+
INCIDENT_FORM_TITLE_MAX_LENGTH,
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/*
|
|
301
|
+
* IncidentFormModel.test.ts pins the IncidentFormSubmission columns to the
|
|
302
|
+
* same lengths.
|
|
303
|
+
*/
|
|
304
|
+
test("the reporter's name and email are capped at 100", () => {
|
|
305
|
+
expect(INCIDENT_FORM_REPORTER_NAME_MAX_LENGTH).toBe(100);
|
|
306
|
+
expect(INCIDENT_FORM_REPORTER_EMAIL_MAX_LENGTH).toBe(100);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("the description and custom field text caps", () => {
|
|
310
|
+
expect(INCIDENT_FORM_DESCRIPTION_MAX_LENGTH).toBe(20000);
|
|
311
|
+
expect(INCIDENT_FORM_CUSTOM_FIELD_TEXT_MAX_LENGTH).toBe(10000);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("the question labels", () => {
|
|
316
|
+
test("are the public page's field titles", () => {
|
|
317
|
+
expect(INCIDENT_FORM_QUESTION_LABELS).toEqual({
|
|
318
|
+
title: "Title",
|
|
319
|
+
description: "Description",
|
|
320
|
+
severity: "Severity",
|
|
321
|
+
reporterName: "Your Name",
|
|
322
|
+
reporterEmail: "Your Email",
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("errors are worded as the browser's own checks word them", () => {
|
|
327
|
+
const validationSource: string = fs.readFileSync(
|
|
328
|
+
path.join(__dirname, "../../../UI/Components/Forms/Validation.ts"),
|
|
329
|
+
"utf8",
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// The templates the server fills in, straight from the form's checks.
|
|
333
|
+
expect(validationSource).toContain('"{{field}} is required."');
|
|
334
|
+
expect(validationSource).toContain(
|
|
335
|
+
'"{{field}} cannot be more than {{maxLength}} characters."',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(errorsOf(validate(submission({ title: "" })))).toEqual([
|
|
339
|
+
"Title is required.",
|
|
340
|
+
]);
|
|
341
|
+
expect(
|
|
342
|
+
errorsOf(
|
|
343
|
+
validate(
|
|
344
|
+
submission({ title: "x".repeat(INCIDENT_FORM_TITLE_MAX_LENGTH + 1) }),
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
).toEqual(["Title cannot be more than 500 characters."]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("a required yes/no answered no reads as the dashboard's must-be-checked message", () => {
|
|
351
|
+
const name: string = "I Have Checked The Status Page";
|
|
352
|
+
|
|
353
|
+
expect(
|
|
354
|
+
errorsOf(
|
|
355
|
+
validate(
|
|
356
|
+
submission({
|
|
357
|
+
customFields: { ...REQUIRED_ANSWERS, [name]: false },
|
|
358
|
+
}),
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
).toEqual([
|
|
362
|
+
CUSTOM_FIELD_MUST_BE_CHECKED_MESSAGE.replace("{{field}}", name),
|
|
363
|
+
]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("getPublicIncidentFormFields: what the page is told about each question", () => {
|
|
368
|
+
test("names, types, help text and whether it is required - nothing else", () => {
|
|
369
|
+
expect(
|
|
370
|
+
getPublicIncidentFormFields([
|
|
371
|
+
{
|
|
372
|
+
name: "Impact",
|
|
373
|
+
description: "How bad is it?",
|
|
374
|
+
customFieldType: CustomFieldType.Text,
|
|
375
|
+
isRequiredOnCreate: true,
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "Region",
|
|
379
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
380
|
+
dropdownOptions: "EU\nUS",
|
|
381
|
+
isRequiredOnCreate: false,
|
|
382
|
+
},
|
|
383
|
+
]),
|
|
384
|
+
).toEqual([
|
|
385
|
+
{
|
|
386
|
+
name: "Impact",
|
|
387
|
+
description: "How bad is it?",
|
|
388
|
+
customFieldType: CustomFieldType.Text,
|
|
389
|
+
isRequired: true,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "Region",
|
|
393
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
394
|
+
dropdownOptions: "EU\nUS",
|
|
395
|
+
isRequired: false,
|
|
396
|
+
},
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("an IncidentCustomField row can be passed as it is, and none of its other columns leak", () => {
|
|
401
|
+
const row: IncidentCustomField = new IncidentCustomField();
|
|
402
|
+
row._id = "0b0b0b0b-0000-4000-8000-000000000001";
|
|
403
|
+
row.projectId = ObjectID.generate();
|
|
404
|
+
row.name = "Region";
|
|
405
|
+
row.description = "Where it happened";
|
|
406
|
+
row.customFieldType = CustomFieldType.MultiSelectDropdown;
|
|
407
|
+
row.dropdownOptions = "EU\nUS";
|
|
408
|
+
row.variableKey = "region";
|
|
409
|
+
row.sortOrder = 2;
|
|
410
|
+
row.showOnCreate = false;
|
|
411
|
+
row.isRequiredOnCreate = true;
|
|
412
|
+
row.includeInSubscriberNotifications = true;
|
|
413
|
+
|
|
414
|
+
const [field] = getPublicIncidentFormFields(
|
|
415
|
+
getIncidentFormAskedDefinitions([row], { region: "Required" }),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(field).toEqual({
|
|
419
|
+
name: "Region",
|
|
420
|
+
description: "Where it happened",
|
|
421
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
422
|
+
dropdownOptions: "EU\nUS",
|
|
423
|
+
isRequired: true,
|
|
424
|
+
});
|
|
425
|
+
expect(Object.keys(field!).sort()).toEqual(
|
|
426
|
+
[
|
|
427
|
+
"customFieldType",
|
|
428
|
+
"description",
|
|
429
|
+
"dropdownOptions",
|
|
430
|
+
"isRequired",
|
|
431
|
+
"name",
|
|
432
|
+
].sort(),
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a dropdown's options are only sent for a dropdown", () => {
|
|
437
|
+
const [text] = getPublicIncidentFormFields([
|
|
438
|
+
{
|
|
439
|
+
name: "Once A Dropdown",
|
|
440
|
+
customFieldType: CustomFieldType.Text,
|
|
441
|
+
dropdownOptions: "Secret option\nAnother",
|
|
442
|
+
},
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
expect(text).not.toHaveProperty("dropdownOptions");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test.each([[undefined], [null], ["Colour"], [""]])(
|
|
449
|
+
"a field whose type is %j is asked as Text",
|
|
450
|
+
(customFieldType: string | null | undefined) => {
|
|
451
|
+
expect(
|
|
452
|
+
getPublicIncidentFormFields([{ name: "Odd", customFieldType }])[0]!
|
|
453
|
+
.customFieldType,
|
|
454
|
+
).toBe(CustomFieldType.Text);
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
test("blank help text and blank options are left out", () => {
|
|
459
|
+
const [field] = getPublicIncidentFormFields([
|
|
460
|
+
{
|
|
461
|
+
name: "Region",
|
|
462
|
+
description: " ",
|
|
463
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
464
|
+
dropdownOptions: " \n ",
|
|
465
|
+
},
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
expect(field).toEqual({
|
|
469
|
+
name: "Region",
|
|
470
|
+
customFieldType: CustomFieldType.Dropdown,
|
|
471
|
+
isRequired: false,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("a field without a name is left out, and so is a second field with the same name", () => {
|
|
476
|
+
expect(
|
|
477
|
+
getPublicIncidentFormFields([
|
|
478
|
+
{ name: "", customFieldType: CustomFieldType.Text },
|
|
479
|
+
{ name: null, customFieldType: CustomFieldType.Text },
|
|
480
|
+
{ customFieldType: CustomFieldType.Text },
|
|
481
|
+
{
|
|
482
|
+
name: "Impact",
|
|
483
|
+
customFieldType: CustomFieldType.Text,
|
|
484
|
+
isRequiredOnCreate: true,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "Impact",
|
|
488
|
+
customFieldType: CustomFieldType.Number,
|
|
489
|
+
isRequiredOnCreate: false,
|
|
490
|
+
},
|
|
491
|
+
]),
|
|
492
|
+
).toEqual([
|
|
493
|
+
{
|
|
494
|
+
name: "Impact",
|
|
495
|
+
customFieldType: CustomFieldType.Text,
|
|
496
|
+
isRequired: true,
|
|
497
|
+
},
|
|
498
|
+
]);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("keeps the order it is given", () => {
|
|
502
|
+
expect(
|
|
503
|
+
getPublicIncidentFormFields([
|
|
504
|
+
{ name: "B" },
|
|
505
|
+
{ name: "A" },
|
|
506
|
+
{ name: "C" },
|
|
507
|
+
]).map((field: PublicIncidentFormField): string => {
|
|
508
|
+
return field.name;
|
|
509
|
+
}),
|
|
510
|
+
).toEqual(["B", "A", "C"]);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("nothing asked, nothing shown", () => {
|
|
514
|
+
expect(getPublicIncidentFormFields([])).toEqual([]);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("getPublicIncidentForm: what the page is told about the form", () => {
|
|
519
|
+
type StoredForm = PublicIncidentFormSource & Record<string, unknown>;
|
|
520
|
+
|
|
521
|
+
type StoredFormFunction = () => StoredForm;
|
|
522
|
+
|
|
523
|
+
const SHARE_KEY: string = "0d0d0d0d-0000-4000-8000-000000000001";
|
|
524
|
+
|
|
525
|
+
/*
|
|
526
|
+
* A form with every column set, as the submit route would load it.
|
|
527
|
+
* IncidentFormModel.test.ts passes a real IncidentForm row the same way.
|
|
528
|
+
*/
|
|
529
|
+
const storedForm: StoredFormFunction = (): StoredForm => {
|
|
530
|
+
return {
|
|
531
|
+
_id: "0c0c0c0c-0000-4000-8000-000000000001",
|
|
532
|
+
projectId: ObjectID.generate(),
|
|
533
|
+
name: "Report a Security Concern",
|
|
534
|
+
description: "Tell us what you saw.",
|
|
535
|
+
isEnabled: true,
|
|
536
|
+
shareKey: new ObjectID(SHARE_KEY),
|
|
537
|
+
incidentSeverityId: new ObjectID(SEVERITY_LOW.toUpperCase()),
|
|
538
|
+
allowReporterToChooseSeverity: true,
|
|
539
|
+
incidentTemplateId: ObjectID.generate(),
|
|
540
|
+
descriptionSetting: IncidentFormFieldSetting.Required,
|
|
541
|
+
customFieldSettings: { impact: "Required" },
|
|
542
|
+
isReporterDetailsRequired: false,
|
|
543
|
+
successMessage: "Thanks!",
|
|
544
|
+
ipWhitelist: "10.0.0.0/8",
|
|
545
|
+
createdByUserId: ObjectID.generate(),
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
test("a stored form can be passed as it is, and only the public subset comes out", () => {
|
|
550
|
+
const publicForm: PublicIncidentForm = getPublicIncidentForm({
|
|
551
|
+
form: storedForm(),
|
|
552
|
+
askedDefinitions: [
|
|
553
|
+
{
|
|
554
|
+
name: "Impact",
|
|
555
|
+
customFieldType: CustomFieldType.Text,
|
|
556
|
+
isRequiredOnCreate: true,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
severities: SEVERITIES,
|
|
560
|
+
isCaptchaRequired: true,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
expect(publicForm).toEqual({
|
|
564
|
+
name: "Report a Security Concern",
|
|
565
|
+
description: "Tell us what you saw.",
|
|
566
|
+
descriptionSetting: IncidentFormFieldSetting.Required,
|
|
567
|
+
isReporterDetailsRequired: false,
|
|
568
|
+
severities: [
|
|
569
|
+
{ _id: SEVERITY_HIGH, name: "High", color: "#ff0000" },
|
|
570
|
+
{ _id: SEVERITY_LOW, name: "Low" },
|
|
571
|
+
],
|
|
572
|
+
defaultIncidentSeverityId: SEVERITY_LOW,
|
|
573
|
+
customFields: [
|
|
574
|
+
{
|
|
575
|
+
name: "Impact",
|
|
576
|
+
customFieldType: CustomFieldType.Text,
|
|
577
|
+
isRequired: true,
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
isCaptchaRequired: true,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// The link key, the project, the template and the allowlist stay home.
|
|
584
|
+
const serialized: string = JSON.stringify(publicForm);
|
|
585
|
+
|
|
586
|
+
for (const secret of [
|
|
587
|
+
SHARE_KEY,
|
|
588
|
+
"10.0.0.0/8",
|
|
589
|
+
"Thanks!",
|
|
590
|
+
"projectId",
|
|
591
|
+
"shareKey",
|
|
592
|
+
"incidentTemplateId",
|
|
593
|
+
"ipWhitelist",
|
|
594
|
+
"customFieldSettings",
|
|
595
|
+
]) {
|
|
596
|
+
expect(serialized).not.toContain(secret);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("the severities are only listed when the reporter may choose one", () => {
|
|
601
|
+
const form: StoredForm = storedForm();
|
|
602
|
+
form.allowReporterToChooseSeverity = false;
|
|
603
|
+
|
|
604
|
+
const publicForm: PublicIncidentForm = getPublicIncidentForm({
|
|
605
|
+
form: form,
|
|
606
|
+
askedDefinitions: [],
|
|
607
|
+
severities: SEVERITIES,
|
|
608
|
+
isCaptchaRequired: false,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
expect(publicForm).not.toHaveProperty("severities");
|
|
612
|
+
expect(publicForm).not.toHaveProperty("defaultIncidentSeverityId");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("the form's own severity is only preselected when it is one of those listed", () => {
|
|
616
|
+
const form: StoredForm = storedForm();
|
|
617
|
+
form.incidentSeverityId = ObjectID.generate();
|
|
618
|
+
|
|
619
|
+
const publicForm: PublicIncidentForm = getPublicIncidentForm({
|
|
620
|
+
form: form,
|
|
621
|
+
askedDefinitions: [],
|
|
622
|
+
severities: SEVERITIES,
|
|
623
|
+
isCaptchaRequired: false,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
expect(publicForm.severities).toHaveLength(2);
|
|
627
|
+
expect(publicForm).not.toHaveProperty("defaultIncidentSeverityId");
|
|
628
|
+
|
|
629
|
+
delete form.incidentSeverityId;
|
|
630
|
+
|
|
631
|
+
expect(
|
|
632
|
+
getPublicIncidentForm({
|
|
633
|
+
form: form,
|
|
634
|
+
askedDefinitions: [],
|
|
635
|
+
severities: SEVERITIES,
|
|
636
|
+
isCaptchaRequired: false,
|
|
637
|
+
}),
|
|
638
|
+
).not.toHaveProperty("defaultIncidentSeverityId");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("only the three properties of each severity are listed, and a severity with no id is not", () => {
|
|
642
|
+
const publicForm: PublicIncidentForm = getPublicIncidentForm({
|
|
643
|
+
form: { allowReporterToChooseSeverity: true },
|
|
644
|
+
askedDefinitions: [],
|
|
645
|
+
severities: [
|
|
646
|
+
{
|
|
647
|
+
_id: SEVERITY_HIGH,
|
|
648
|
+
name: "High",
|
|
649
|
+
projectId: "somewhere",
|
|
650
|
+
order: 1,
|
|
651
|
+
} as unknown as PublicIncidentFormSeverity,
|
|
652
|
+
{ _id: "", name: "No Id" },
|
|
653
|
+
],
|
|
654
|
+
isCaptchaRequired: false,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
expect(publicForm.severities).toEqual([
|
|
658
|
+
{ _id: SEVERITY_HIGH, name: "High" },
|
|
659
|
+
]);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("defaults for a form read without its settings", () => {
|
|
663
|
+
expect(
|
|
664
|
+
getPublicIncidentForm({
|
|
665
|
+
form: {},
|
|
666
|
+
askedDefinitions: [],
|
|
667
|
+
severities: SEVERITIES,
|
|
668
|
+
isCaptchaRequired: false,
|
|
669
|
+
}),
|
|
670
|
+
).toEqual({
|
|
671
|
+
name: "",
|
|
672
|
+
descriptionSetting: IncidentFormFieldSetting.Optional,
|
|
673
|
+
isReporterDetailsRequired: true,
|
|
674
|
+
customFields: [],
|
|
675
|
+
isCaptchaRequired: false,
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("a description of only whitespace is not sent, and an unknown description setting reads as Optional", () => {
|
|
680
|
+
expect(
|
|
681
|
+
getPublicIncidentForm({
|
|
682
|
+
form: { description: " \n ", descriptionSetting: "Mandatory" },
|
|
683
|
+
askedDefinitions: [],
|
|
684
|
+
severities: [],
|
|
685
|
+
isCaptchaRequired: false,
|
|
686
|
+
}),
|
|
687
|
+
).toEqual({
|
|
688
|
+
name: "",
|
|
689
|
+
descriptionSetting: IncidentFormFieldSetting.Optional,
|
|
690
|
+
isReporterDetailsRequired: true,
|
|
691
|
+
customFields: [],
|
|
692
|
+
isCaptchaRequired: false,
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("validateIncidentFormSubmission: the shape of the request", () => {
|
|
698
|
+
test.each([
|
|
699
|
+
["null", null],
|
|
700
|
+
["undefined", undefined],
|
|
701
|
+
["a string", "title=Down"],
|
|
702
|
+
["an array", [{ title: "Down" }]],
|
|
703
|
+
["a number", 5],
|
|
704
|
+
])("refuses %s", (_label: string, data: unknown) => {
|
|
705
|
+
expect(errorsOf(validate(data))).toEqual([
|
|
706
|
+
"The submission must be an object holding the form's answers.",
|
|
707
|
+
]);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
test("a complete submission passes, cleaned", () => {
|
|
711
|
+
expect(valueOf(validate(submission()))).toEqual({
|
|
712
|
+
title: "Checkout fails",
|
|
713
|
+
reporterName: "Jane Doe",
|
|
714
|
+
reporterEmail: "jane@example.com",
|
|
715
|
+
customFields: {
|
|
716
|
+
Impact: "Checkout is down",
|
|
717
|
+
"I Have Checked The Status Page": true,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("nothing but the answers gets through: a projectId, a state or a privacy flag is dropped", () => {
|
|
723
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
724
|
+
validate(
|
|
725
|
+
submission({
|
|
726
|
+
projectId: ObjectID.generate().toString(),
|
|
727
|
+
currentIncidentStateId: ObjectID.generate().toString(),
|
|
728
|
+
createdIncidentTemplateId: ObjectID.generate().toString(),
|
|
729
|
+
isPrivate: true,
|
|
730
|
+
isVisibleOnStatusPage: true,
|
|
731
|
+
createdByUserId: ObjectID.generate().toString(),
|
|
732
|
+
_id: ObjectID.generate().toString(),
|
|
733
|
+
monitors: [ObjectID.generate().toString()],
|
|
734
|
+
}),
|
|
735
|
+
),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
expect(Object.keys(value).sort()).toEqual(
|
|
739
|
+
["customFields", "reporterEmail", "reporterName", "title"].sort(),
|
|
740
|
+
);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("every problem is reported at once, in the order of the form", () => {
|
|
744
|
+
expect(
|
|
745
|
+
errorsOf(
|
|
746
|
+
validate({
|
|
747
|
+
title: "",
|
|
748
|
+
reporterName: "",
|
|
749
|
+
reporterEmail: "not an email",
|
|
750
|
+
customFields: {},
|
|
751
|
+
}),
|
|
752
|
+
),
|
|
753
|
+
).toEqual([
|
|
754
|
+
"Title is required.",
|
|
755
|
+
"Your Name is required.",
|
|
756
|
+
"Your Email is not a valid email address.",
|
|
757
|
+
"Impact is required.",
|
|
758
|
+
"I Have Checked The Status Page must be checked.",
|
|
759
|
+
]);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("never changes the request it is given", () => {
|
|
763
|
+
const data: JSONObject = deepFreeze(
|
|
764
|
+
submission({
|
|
765
|
+
title: " Checkout fails ",
|
|
766
|
+
customFields: {
|
|
767
|
+
...REQUIRED_ANSWERS,
|
|
768
|
+
Systems: ["API", "API", " Web "],
|
|
769
|
+
},
|
|
770
|
+
}),
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
expect(valueOf(validate(data)).customFields["Systems"]).toEqual([
|
|
774
|
+
"API",
|
|
775
|
+
"Web",
|
|
776
|
+
]);
|
|
777
|
+
expect(data["title"]).toBe(" Checkout fails ");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("missing settings and lists are read as their defaults", () => {
|
|
781
|
+
const result: IncidentFormSubmissionValidationResult =
|
|
782
|
+
validateIncidentFormSubmission({
|
|
783
|
+
form: undefined as unknown as IncidentFormSubmissionRules,
|
|
784
|
+
askedDefinitions:
|
|
785
|
+
undefined as unknown as Array<IncidentFormAskedDefinition>,
|
|
786
|
+
severities: undefined as unknown as Array<{ _id: string }>,
|
|
787
|
+
data: { title: "Down", incidentSeverityId: SEVERITY_HIGH },
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Reporter details are required by default; nothing else is asked.
|
|
791
|
+
expect(errorsOf(result)).toEqual([
|
|
792
|
+
"Your Name is required.",
|
|
793
|
+
"Your Email is required.",
|
|
794
|
+
]);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
describe("validateIncidentFormSubmission: the title", () => {
|
|
799
|
+
test.each([
|
|
800
|
+
["missing", undefined],
|
|
801
|
+
["null", null],
|
|
802
|
+
["empty", ""],
|
|
803
|
+
["only spaces", " "],
|
|
804
|
+
["only line breaks and tabs", "\n\t\r\n"],
|
|
805
|
+
])("is required: %s is refused", (_label: string, title: unknown) => {
|
|
806
|
+
const data: JSONObject = submission();
|
|
807
|
+
|
|
808
|
+
if (title === undefined) {
|
|
809
|
+
delete data["title"];
|
|
810
|
+
} else {
|
|
811
|
+
data["title"] = title as JSONObject[string];
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
expect(errorsOf(validate(data))).toEqual(["Title is required."]);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test.each([[5], [true], [{ text: "Down" }], [["Down"]]])(
|
|
818
|
+
"must be text: %j is refused once, not also as missing",
|
|
819
|
+
(title: unknown) => {
|
|
820
|
+
expect(
|
|
821
|
+
errorsOf(validate(submission({ title: title as JSONObject[string] }))),
|
|
822
|
+
).toEqual(["Title must be text."]);
|
|
823
|
+
},
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
test("is trimmed, and its line breaks, tabs and control characters become single spaces", () => {
|
|
827
|
+
expect(
|
|
828
|
+
valueOf(
|
|
829
|
+
validate(
|
|
830
|
+
submission({ title: " Checkout\n\n down\tin\u0000EU\u007f " }),
|
|
831
|
+
),
|
|
832
|
+
).title,
|
|
833
|
+
).toBe("Checkout down in EU");
|
|
834
|
+
expect(
|
|
835
|
+
valueOf(validate(submission({ title: "Checkout down" }))).title,
|
|
836
|
+
).toBe("Checkout down");
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
test("keeps characters that are not control characters, non-breaking spaces and emoji included", () => {
|
|
840
|
+
expect(
|
|
841
|
+
valueOf(validate(submission({ title: "Café down 🔥 (EU)" }))).title,
|
|
842
|
+
).toBe("Café down 🔥 (EU)");
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test("may be exactly 500 characters, measured after trimming", () => {
|
|
846
|
+
const longest: string = "x".repeat(INCIDENT_FORM_TITLE_MAX_LENGTH);
|
|
847
|
+
|
|
848
|
+
expect(
|
|
849
|
+
valueOf(validate(submission({ title: ` ${longest} ` }))).title,
|
|
850
|
+
).toBe(longest);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test("may not be 501", () => {
|
|
854
|
+
expect(
|
|
855
|
+
errorsOf(
|
|
856
|
+
validate(
|
|
857
|
+
submission({ title: "x".repeat(INCIDENT_FORM_TITLE_MAX_LENGTH + 1) }),
|
|
858
|
+
),
|
|
859
|
+
),
|
|
860
|
+
).toEqual(["Title cannot be more than 500 characters."]);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
describe("validateIncidentFormSubmission: the description", () => {
|
|
865
|
+
type RulesFunction = (
|
|
866
|
+
setting: IncidentFormFieldSetting | string | undefined,
|
|
867
|
+
) => IncidentFormSubmissionRules;
|
|
868
|
+
|
|
869
|
+
const rules: RulesFunction = (
|
|
870
|
+
setting: IncidentFormFieldSetting | string | undefined,
|
|
871
|
+
): IncidentFormSubmissionRules => {
|
|
872
|
+
return { ...DEFAULT_RULES, descriptionSetting: setting };
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
test("an optional description may be left out, or left empty", () => {
|
|
876
|
+
for (const description of [undefined, null, "", " ", "\n\n \n"]) {
|
|
877
|
+
const data: JSONObject = submission();
|
|
878
|
+
|
|
879
|
+
if (description !== undefined) {
|
|
880
|
+
data["description"] = description;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
expect(
|
|
884
|
+
valueOf(
|
|
885
|
+
validate(data, { form: rules(IncidentFormFieldSetting.Optional) }),
|
|
886
|
+
),
|
|
887
|
+
).not.toHaveProperty("description");
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test("a required description may not", () => {
|
|
892
|
+
for (const description of [undefined, "", " \n "]) {
|
|
893
|
+
const data: JSONObject = submission();
|
|
894
|
+
|
|
895
|
+
if (description !== undefined) {
|
|
896
|
+
data["description"] = description;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
expect(
|
|
900
|
+
errorsOf(
|
|
901
|
+
validate(data, { form: rules(IncidentFormFieldSetting.Required) }),
|
|
902
|
+
),
|
|
903
|
+
).toEqual(["Description is required."]);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("a hidden description is not even read: whatever is sent is dropped", () => {
|
|
908
|
+
for (const description of [
|
|
909
|
+
"Sneaked in",
|
|
910
|
+
5,
|
|
911
|
+
{ nested: true },
|
|
912
|
+
"x".repeat(INCIDENT_FORM_DESCRIPTION_MAX_LENGTH + 100),
|
|
913
|
+
]) {
|
|
914
|
+
expect(
|
|
915
|
+
valueOf(
|
|
916
|
+
validate(
|
|
917
|
+
submission({ description: description as JSONObject[string] }),
|
|
918
|
+
{ form: rules(IncidentFormFieldSetting.Hidden) },
|
|
919
|
+
),
|
|
920
|
+
),
|
|
921
|
+
).not.toHaveProperty("description");
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test("keeps the reporter's Markdown: line breaks inside, and the first line's indentation", () => {
|
|
926
|
+
expect(
|
|
927
|
+
valueOf(
|
|
928
|
+
validate(
|
|
929
|
+
submission({
|
|
930
|
+
description:
|
|
931
|
+
"\n\n npm run build\n exit 1\n\n**Since 10:00** \n\n",
|
|
932
|
+
}),
|
|
933
|
+
),
|
|
934
|
+
).description,
|
|
935
|
+
).toBe(" npm run build\n exit 1\n\n**Since 10:00**");
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("drops NUL characters, which Postgres cannot store", () => {
|
|
939
|
+
expect(
|
|
940
|
+
valueOf(validate(submission({ description: "a\u0000b" }))).description,
|
|
941
|
+
).toBe("ab");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("must be text when it is asked", () => {
|
|
945
|
+
expect(errorsOf(validate(submission({ description: 42 })))).toEqual([
|
|
946
|
+
"Description must be text.",
|
|
947
|
+
]);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("may be 20000 characters, not 20001", () => {
|
|
951
|
+
expect(
|
|
952
|
+
valueOf(
|
|
953
|
+
validate(
|
|
954
|
+
submission({
|
|
955
|
+
description: "x".repeat(INCIDENT_FORM_DESCRIPTION_MAX_LENGTH),
|
|
956
|
+
}),
|
|
957
|
+
),
|
|
958
|
+
).description,
|
|
959
|
+
).toHaveLength(INCIDENT_FORM_DESCRIPTION_MAX_LENGTH);
|
|
960
|
+
|
|
961
|
+
expect(
|
|
962
|
+
errorsOf(
|
|
963
|
+
validate(
|
|
964
|
+
submission({
|
|
965
|
+
description: "x".repeat(INCIDENT_FORM_DESCRIPTION_MAX_LENGTH + 1),
|
|
966
|
+
}),
|
|
967
|
+
),
|
|
968
|
+
),
|
|
969
|
+
).toEqual(["Description cannot be more than 20000 characters."]);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test.each([[undefined], ["Mandatory"], ["required"], [null]])(
|
|
973
|
+
"a description setting of %j is read as Optional",
|
|
974
|
+
(setting: unknown) => {
|
|
975
|
+
const form: IncidentFormSubmissionRules = {
|
|
976
|
+
...DEFAULT_RULES,
|
|
977
|
+
descriptionSetting: setting as string,
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
expect(valueOf(validate(submission(), { form })).title).toBe(
|
|
981
|
+
"Checkout fails",
|
|
982
|
+
);
|
|
983
|
+
expect(
|
|
984
|
+
valueOf(validate(submission({ description: "Kept" }), { form }))
|
|
985
|
+
.description,
|
|
986
|
+
).toBe("Kept");
|
|
987
|
+
},
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
describe("validateIncidentFormSubmission: the severity", () => {
|
|
992
|
+
const CHOOSING: IncidentFormSubmissionRules = {
|
|
993
|
+
...DEFAULT_RULES,
|
|
994
|
+
allowReporterToChooseSeverity: true,
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
test("is not read when the form does not let the reporter choose", () => {
|
|
998
|
+
for (const incidentSeverityId of [
|
|
999
|
+
SEVERITY_HIGH,
|
|
1000
|
+
"not an id",
|
|
1001
|
+
5,
|
|
1002
|
+
ObjectID.generate().toString(),
|
|
1003
|
+
]) {
|
|
1004
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1005
|
+
validate(
|
|
1006
|
+
submission({
|
|
1007
|
+
incidentSeverityId: incidentSeverityId as JSONObject[string],
|
|
1008
|
+
}),
|
|
1009
|
+
),
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
expect(value).not.toHaveProperty("incidentSeverityId");
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
test("may be left out, which means the form's own severity", () => {
|
|
1017
|
+
for (const incidentSeverityId of [undefined, null, "", " "]) {
|
|
1018
|
+
const data: JSONObject = submission();
|
|
1019
|
+
|
|
1020
|
+
if (incidentSeverityId !== undefined) {
|
|
1021
|
+
data["incidentSeverityId"] = incidentSeverityId;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
expect(valueOf(validate(data, { form: CHOOSING }))).not.toHaveProperty(
|
|
1025
|
+
"incidentSeverityId",
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
test("one of the listed severities is taken, as listed", () => {
|
|
1031
|
+
expect(
|
|
1032
|
+
valueOf(
|
|
1033
|
+
validate(submission({ incidentSeverityId: SEVERITY_LOW }), {
|
|
1034
|
+
form: CHOOSING,
|
|
1035
|
+
}),
|
|
1036
|
+
).incidentSeverityId,
|
|
1037
|
+
).toBe(SEVERITY_LOW);
|
|
1038
|
+
|
|
1039
|
+
expect(
|
|
1040
|
+
valueOf(
|
|
1041
|
+
validate(
|
|
1042
|
+
submission({
|
|
1043
|
+
incidentSeverityId: ` ${SEVERITY_HIGH.toUpperCase()} `,
|
|
1044
|
+
}),
|
|
1045
|
+
{ form: CHOOSING },
|
|
1046
|
+
),
|
|
1047
|
+
).incidentSeverityId,
|
|
1048
|
+
).toBe(SEVERITY_HIGH);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test.each([
|
|
1052
|
+
["another project's severity", ObjectID.generate().toString()],
|
|
1053
|
+
["something that is not an id", "critical"],
|
|
1054
|
+
["a number", 1],
|
|
1055
|
+
["an object", { _id: SEVERITY_HIGH }],
|
|
1056
|
+
["a list", [SEVERITY_HIGH]],
|
|
1057
|
+
])("%s is refused", (_label: string, incidentSeverityId: unknown) => {
|
|
1058
|
+
expect(
|
|
1059
|
+
errorsOf(
|
|
1060
|
+
validate(
|
|
1061
|
+
submission({
|
|
1062
|
+
incidentSeverityId: incidentSeverityId as JSONObject[string],
|
|
1063
|
+
}),
|
|
1064
|
+
{ form: CHOOSING },
|
|
1065
|
+
),
|
|
1066
|
+
),
|
|
1067
|
+
).toEqual(["Severity must be one of the severities this form lists."]);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test("with no severities to list, any choice is refused", () => {
|
|
1071
|
+
expect(
|
|
1072
|
+
errorsOf(
|
|
1073
|
+
validate(submission({ incidentSeverityId: SEVERITY_HIGH }), {
|
|
1074
|
+
form: CHOOSING,
|
|
1075
|
+
severities: [],
|
|
1076
|
+
}),
|
|
1077
|
+
),
|
|
1078
|
+
).toEqual(["Severity must be one of the severities this form lists."]);
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
describe("validateIncidentFormSubmission: the reporter", () => {
|
|
1083
|
+
const ANONYMOUS: IncidentFormSubmissionRules = {
|
|
1084
|
+
...DEFAULT_RULES,
|
|
1085
|
+
isReporterDetailsRequired: false,
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
test("the name and email are required unless the form says otherwise", () => {
|
|
1089
|
+
const data: JSONObject = submission();
|
|
1090
|
+
delete data["reporterName"];
|
|
1091
|
+
delete data["reporterEmail"];
|
|
1092
|
+
|
|
1093
|
+
expect(errorsOf(validate(data))).toEqual([
|
|
1094
|
+
"Your Name is required.",
|
|
1095
|
+
"Your Email is required.",
|
|
1096
|
+
]);
|
|
1097
|
+
|
|
1098
|
+
// Undefined is the column's default: details required.
|
|
1099
|
+
expect(
|
|
1100
|
+
errorsOf(
|
|
1101
|
+
validate(data, {
|
|
1102
|
+
form: { ...DEFAULT_RULES, isReporterDetailsRequired: undefined },
|
|
1103
|
+
}),
|
|
1104
|
+
),
|
|
1105
|
+
).toEqual(["Your Name is required.", "Your Email is required."]);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
test("an anonymous report leaves both out", () => {
|
|
1109
|
+
const data: JSONObject = submission({
|
|
1110
|
+
reporterName: " ",
|
|
1111
|
+
reporterEmail: "",
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1115
|
+
validate(data, { form: ANONYMOUS }),
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
expect(value).not.toHaveProperty("reporterName");
|
|
1119
|
+
expect(value).not.toHaveProperty("reporterEmail");
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
test("details given on an anonymous form are still kept, and still checked", () => {
|
|
1123
|
+
expect(valueOf(validate(submission(), { form: ANONYMOUS }))).toMatchObject({
|
|
1124
|
+
reporterName: "Jane Doe",
|
|
1125
|
+
reporterEmail: "jane@example.com",
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
expect(
|
|
1129
|
+
errorsOf(
|
|
1130
|
+
validate(submission({ reporterEmail: "jane" }), { form: ANONYMOUS }),
|
|
1131
|
+
),
|
|
1132
|
+
).toEqual(["Your Email is not a valid email address."]);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("the name is cleaned to one line", () => {
|
|
1136
|
+
expect(
|
|
1137
|
+
valueOf(validate(submission({ reporterName: " Jane\n\tDoe " })))
|
|
1138
|
+
.reporterName,
|
|
1139
|
+
).toBe("Jane Doe");
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test("the name may be 100 characters, not 101", () => {
|
|
1143
|
+
expect(
|
|
1144
|
+
valueOf(
|
|
1145
|
+
validate(
|
|
1146
|
+
submission({
|
|
1147
|
+
reporterName: "n".repeat(INCIDENT_FORM_REPORTER_NAME_MAX_LENGTH),
|
|
1148
|
+
}),
|
|
1149
|
+
),
|
|
1150
|
+
).reporterName,
|
|
1151
|
+
).toHaveLength(INCIDENT_FORM_REPORTER_NAME_MAX_LENGTH);
|
|
1152
|
+
|
|
1153
|
+
expect(
|
|
1154
|
+
errorsOf(
|
|
1155
|
+
validate(
|
|
1156
|
+
submission({
|
|
1157
|
+
reporterName: "n".repeat(
|
|
1158
|
+
INCIDENT_FORM_REPORTER_NAME_MAX_LENGTH + 1,
|
|
1159
|
+
),
|
|
1160
|
+
}),
|
|
1161
|
+
),
|
|
1162
|
+
),
|
|
1163
|
+
).toEqual(["Your Name cannot be more than 100 characters."]);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test("the email is trimmed and lowercased, as Email stores it", () => {
|
|
1167
|
+
expect(
|
|
1168
|
+
valueOf(
|
|
1169
|
+
validate(submission({ reporterEmail: " Jane.Doe@Example.COM\n" })),
|
|
1170
|
+
).reporterEmail,
|
|
1171
|
+
).toBe("jane.doe@example.com");
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test.each([
|
|
1175
|
+
["no at sign", "jane.example.com"],
|
|
1176
|
+
["no domain", "jane@"],
|
|
1177
|
+
["no mailbox", "@example.com"],
|
|
1178
|
+
["no dot in the domain", "jane@localhost"],
|
|
1179
|
+
["a space", "jane doe@example.com"],
|
|
1180
|
+
["two at signs", "jane@@example.com"],
|
|
1181
|
+
["a display name", "Jane <jane@example.com>"],
|
|
1182
|
+
["trailing words", "jane@example.com please reply"],
|
|
1183
|
+
["leading words", "mail jane@example.com"],
|
|
1184
|
+
["a line break inside", "jane@exam\nple.com"],
|
|
1185
|
+
["a trailing dot", "jane@example.com."],
|
|
1186
|
+
["a leading dot", ".jane@example.com"],
|
|
1187
|
+
])(
|
|
1188
|
+
"an email with %s is refused, even though Email finds an address in it",
|
|
1189
|
+
(_label: string, email: string) => {
|
|
1190
|
+
expect(errorsOf(validate(submission({ reporterEmail: email })))).toEqual([
|
|
1191
|
+
"Your Email is not a valid email address.",
|
|
1192
|
+
]);
|
|
1193
|
+
},
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
test.each([
|
|
1197
|
+
["jane+incidents@example.com"],
|
|
1198
|
+
["j.o'hara@sub.example.co.uk"],
|
|
1199
|
+
["ops_team-1@example-mail.io"],
|
|
1200
|
+
])("an ordinary address like %s passes", (email: string) => {
|
|
1201
|
+
expect(
|
|
1202
|
+
valueOf(validate(submission({ reporterEmail: email }))).reporterEmail,
|
|
1203
|
+
).toBe(email);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("the email may be 100 characters, not 101 - and a long one is only told it is too long", () => {
|
|
1207
|
+
const domain: string = "@example.com";
|
|
1208
|
+
const longest: string = `${"a".repeat(
|
|
1209
|
+
INCIDENT_FORM_REPORTER_EMAIL_MAX_LENGTH - domain.length,
|
|
1210
|
+
)}${domain}`;
|
|
1211
|
+
|
|
1212
|
+
expect(
|
|
1213
|
+
valueOf(validate(submission({ reporterEmail: longest }))).reporterEmail,
|
|
1214
|
+
).toBe(longest);
|
|
1215
|
+
|
|
1216
|
+
expect(
|
|
1217
|
+
errorsOf(validate(submission({ reporterEmail: `a${longest}` }))),
|
|
1218
|
+
).toEqual(["Your Email cannot be more than 100 characters."]);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
test.each([
|
|
1222
|
+
[5],
|
|
1223
|
+
[true],
|
|
1224
|
+
[{ email: "jane@example.com" }],
|
|
1225
|
+
[["jane@example.com"]],
|
|
1226
|
+
])(
|
|
1227
|
+
"the name and email must be text: %j is refused once each",
|
|
1228
|
+
(value: unknown) => {
|
|
1229
|
+
expect(
|
|
1230
|
+
errorsOf(
|
|
1231
|
+
validate(
|
|
1232
|
+
submission({
|
|
1233
|
+
reporterName: value as JSONObject[string],
|
|
1234
|
+
reporterEmail: value as JSONObject[string],
|
|
1235
|
+
}),
|
|
1236
|
+
),
|
|
1237
|
+
),
|
|
1238
|
+
).toEqual(["Your Name must be text.", "Your Email must be text."]);
|
|
1239
|
+
},
|
|
1240
|
+
);
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
describe("validateIncidentFormSubmission: custom field answers", () => {
|
|
1244
|
+
type AnswerFunction = (
|
|
1245
|
+
answers: JSONObject,
|
|
1246
|
+
) => IncidentFormSubmissionValidationResult;
|
|
1247
|
+
|
|
1248
|
+
const answer: AnswerFunction = (
|
|
1249
|
+
answers: JSONObject,
|
|
1250
|
+
): IncidentFormSubmissionValidationResult => {
|
|
1251
|
+
return validate(
|
|
1252
|
+
submission({ customFields: { ...REQUIRED_ANSWERS, ...answers } }),
|
|
1253
|
+
);
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
test("answers are an object keyed by field name", () => {
|
|
1257
|
+
for (const customFields of ["Impact=down", ["Checkout is down"], 5, true]) {
|
|
1258
|
+
expect(
|
|
1259
|
+
errorsOf(
|
|
1260
|
+
validate(
|
|
1261
|
+
submission({ customFields: customFields as JSONObject[string] }),
|
|
1262
|
+
),
|
|
1263
|
+
),
|
|
1264
|
+
).toEqual([
|
|
1265
|
+
"The custom field answers must be an object keyed by field name.",
|
|
1266
|
+
"Impact is required.",
|
|
1267
|
+
"I Have Checked The Status Page must be checked.",
|
|
1268
|
+
]);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test("no answers at all only fails the required questions", () => {
|
|
1273
|
+
const data: JSONObject = submission();
|
|
1274
|
+
delete data["customFields"];
|
|
1275
|
+
|
|
1276
|
+
expect(errorsOf(validate(data))).toEqual([
|
|
1277
|
+
"Impact is required.",
|
|
1278
|
+
"I Have Checked The Status Page must be checked.",
|
|
1279
|
+
]);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("an answer to a field the form does not ask is dropped unread", () => {
|
|
1283
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1284
|
+
answer({
|
|
1285
|
+
"Internal Cost Centre": "4411",
|
|
1286
|
+
"Secret Escalation Note": { not: "even valid" },
|
|
1287
|
+
impact: "lowercase name, not the field",
|
|
1288
|
+
}),
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
expect(Object.keys(value.customFields).sort()).toEqual(
|
|
1292
|
+
["I Have Checked The Status Page", "Impact"].sort(),
|
|
1293
|
+
);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
test("a form that asks nothing stores nothing, whatever is sent", () => {
|
|
1297
|
+
expect(
|
|
1298
|
+
valueOf(
|
|
1299
|
+
validate(submission({ customFields: { Impact: "Down", Other: 1 } }), {
|
|
1300
|
+
askedDefinitions: [],
|
|
1301
|
+
}),
|
|
1302
|
+
).customFields,
|
|
1303
|
+
).toEqual({});
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
test("an optional question left empty is left out, so a template's value still applies", () => {
|
|
1307
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1308
|
+
answer({
|
|
1309
|
+
"Steps to Reproduce": " ",
|
|
1310
|
+
Notes: "",
|
|
1311
|
+
"Users Affected": null,
|
|
1312
|
+
Region: "",
|
|
1313
|
+
Systems: [],
|
|
1314
|
+
"Noticed On": " ",
|
|
1315
|
+
}),
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
expect(value.customFields).toEqual(REQUIRED_ANSWERS);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
test("a required text question answered with whitespace is still unanswered", () => {
|
|
1322
|
+
expect(errorsOf(answer({ Impact: " \n\t " }))).toEqual([
|
|
1323
|
+
"Impact is required.",
|
|
1324
|
+
]);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
test("text is cleaned as its field shows it", () => {
|
|
1328
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1329
|
+
answer({
|
|
1330
|
+
Impact: " Checkout\ndown ",
|
|
1331
|
+
"Steps to Reproduce": "\n1. Open checkout\n2. Pay\n\n",
|
|
1332
|
+
Notes: "\n\n stack trace\n",
|
|
1333
|
+
}),
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
expect(value.customFields["Impact"]).toBe("Checkout down");
|
|
1337
|
+
expect(value.customFields["Steps to Reproduce"]).toBe(
|
|
1338
|
+
"1. Open checkout\n2. Pay",
|
|
1339
|
+
);
|
|
1340
|
+
expect(value.customFields["Notes"]).toBe(" stack trace");
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
test("NUL characters are dropped from every text answer", () => {
|
|
1344
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1345
|
+
answer({
|
|
1346
|
+
Impact: "Check\u0000out",
|
|
1347
|
+
Notes: "a\u0000b",
|
|
1348
|
+
Region: "E\u0000U",
|
|
1349
|
+
Systems: ["A\u0000PI"],
|
|
1350
|
+
}),
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
expect(value.customFields).toMatchObject({
|
|
1354
|
+
Impact: "Check out",
|
|
1355
|
+
Notes: "ab",
|
|
1356
|
+
Region: "EU",
|
|
1357
|
+
Systems: ["API"],
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
test("each answer is checked as an API write would check it", () => {
|
|
1362
|
+
expect(
|
|
1363
|
+
errorsOf(
|
|
1364
|
+
answer({
|
|
1365
|
+
"Users Affected": "lots",
|
|
1366
|
+
Region: "APAC",
|
|
1367
|
+
Systems: ["API", "Mobile"],
|
|
1368
|
+
"Noticed On": "yesterday-ish",
|
|
1369
|
+
"Noticed At": "soon",
|
|
1370
|
+
Untyped: { nested: true },
|
|
1371
|
+
}),
|
|
1372
|
+
),
|
|
1373
|
+
).toEqual([
|
|
1374
|
+
'"Users Affected" holds a number, but was sent "lots".',
|
|
1375
|
+
'"APAC" is not one of the options for "Region". Choose one of: "EU", "US".',
|
|
1376
|
+
'"Mobile" is not one of the options for "Systems". Choose from: "API", "Web".',
|
|
1377
|
+
'"Noticed On" holds a date, but was sent "yesterday-ish".',
|
|
1378
|
+
'"Noticed At" holds a date and time, but was sent "soon".',
|
|
1379
|
+
"Untyped takes one answer, not an object.",
|
|
1380
|
+
]);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
/*
|
|
1384
|
+
* An answer is one value - text, a number, a yes/no - or, for a
|
|
1385
|
+
* multi-select, a list of them; nothing the form's page sends is an
|
|
1386
|
+
* object. One is refused before anything reads it: checked as an API
|
|
1387
|
+
* write would check it, it would be turned into text twice first -
|
|
1388
|
+
* seconds for one with a million keys, and far longer for one nested
|
|
1389
|
+
* millions deep.
|
|
1390
|
+
*/
|
|
1391
|
+
test.each([
|
|
1392
|
+
["a text question", "Impact", "Impact takes one answer, not an object."],
|
|
1393
|
+
[
|
|
1394
|
+
"a long text question",
|
|
1395
|
+
"Steps to Reproduce",
|
|
1396
|
+
"Steps to Reproduce takes one answer, not an object.",
|
|
1397
|
+
],
|
|
1398
|
+
["a Markdown question", "Notes", "Notes takes one answer, not an object."],
|
|
1399
|
+
[
|
|
1400
|
+
"a number question",
|
|
1401
|
+
"Users Affected",
|
|
1402
|
+
"Users Affected takes one answer, not an object.",
|
|
1403
|
+
],
|
|
1404
|
+
[
|
|
1405
|
+
"a yes/no question",
|
|
1406
|
+
"Customer Facing",
|
|
1407
|
+
"Customer Facing takes one answer, not an object.",
|
|
1408
|
+
],
|
|
1409
|
+
["a dropdown", "Region", "Region takes one answer, not an object."],
|
|
1410
|
+
[
|
|
1411
|
+
"a multi-select",
|
|
1412
|
+
"Systems",
|
|
1413
|
+
"Systems takes a list of its options, not an object.",
|
|
1414
|
+
],
|
|
1415
|
+
[
|
|
1416
|
+
"a date question",
|
|
1417
|
+
"Noticed On",
|
|
1418
|
+
"Noticed On takes one answer, not an object.",
|
|
1419
|
+
],
|
|
1420
|
+
[
|
|
1421
|
+
"a date and time question",
|
|
1422
|
+
"Noticed At",
|
|
1423
|
+
"Noticed At takes one answer, not an object.",
|
|
1424
|
+
],
|
|
1425
|
+
[
|
|
1426
|
+
"a question with no type",
|
|
1427
|
+
"Untyped",
|
|
1428
|
+
"Untyped takes one answer, not an object.",
|
|
1429
|
+
],
|
|
1430
|
+
])(
|
|
1431
|
+
"refuses an object for %s",
|
|
1432
|
+
(_label: string, name: string, message: string) => {
|
|
1433
|
+
expect(errorsOf(answer({ [name]: { nested: true } }))).toEqual([message]);
|
|
1434
|
+
},
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
test("refuses an empty object as well: it is no more an answer", () => {
|
|
1438
|
+
expect(errorsOf(answer({ Region: {} }))).toEqual([
|
|
1439
|
+
"Region takes one answer, not an object.",
|
|
1440
|
+
]);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("refuses an object without reading any of it", () => {
|
|
1444
|
+
const refuseToBeRead: () => never = (): never => {
|
|
1445
|
+
throw new Error("The answer was read.");
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const unreadable: JSONObject = new Proxy(
|
|
1449
|
+
{},
|
|
1450
|
+
{
|
|
1451
|
+
get: refuseToBeRead,
|
|
1452
|
+
has: refuseToBeRead,
|
|
1453
|
+
ownKeys: refuseToBeRead,
|
|
1454
|
+
getOwnPropertyDescriptor: refuseToBeRead,
|
|
1455
|
+
getPrototypeOf: refuseToBeRead,
|
|
1456
|
+
},
|
|
1457
|
+
) as JSONObject;
|
|
1458
|
+
|
|
1459
|
+
let result: IncidentFormSubmissionValidationResult | undefined;
|
|
1460
|
+
|
|
1461
|
+
expect(() => {
|
|
1462
|
+
result = answer({ Region: unreadable });
|
|
1463
|
+
}).not.toThrow();
|
|
1464
|
+
|
|
1465
|
+
expect(errorsOf(result!)).toEqual([
|
|
1466
|
+
"Region takes one answer, not an object.",
|
|
1467
|
+
]);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
test("refuses an object nested two hundred thousand deep at once, without throwing", () => {
|
|
1471
|
+
const depth: number = 200_000;
|
|
1472
|
+
const nested: JSONObject = JSON.parse(
|
|
1473
|
+
`${'{"a":'.repeat(depth)}1${"}".repeat(depth)}`,
|
|
1474
|
+
) as JSONObject;
|
|
1475
|
+
|
|
1476
|
+
const started: number = Date.now();
|
|
1477
|
+
let result: IncidentFormSubmissionValidationResult | undefined;
|
|
1478
|
+
|
|
1479
|
+
expect(() => {
|
|
1480
|
+
result = answer({ Notes: nested });
|
|
1481
|
+
}).not.toThrow();
|
|
1482
|
+
|
|
1483
|
+
expect(Date.now() - started).toBeLessThan(1000);
|
|
1484
|
+
expect(errorsOf(result!)).toEqual([
|
|
1485
|
+
"Notes takes one answer, not an object.",
|
|
1486
|
+
]);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
test("a null answer is still no answer", () => {
|
|
1490
|
+
expect(errorsOf(answer({ Impact: null }))).toEqual(["Impact is required."]);
|
|
1491
|
+
expect(
|
|
1492
|
+
valueOf(answer({ Region: null })).customFields["Region"],
|
|
1493
|
+
).toBeUndefined();
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
test("good answers of every type are kept", () => {
|
|
1497
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1498
|
+
answer({
|
|
1499
|
+
"Users Affected": "1200",
|
|
1500
|
+
Region: " EU ",
|
|
1501
|
+
Systems: ["Web", "API"],
|
|
1502
|
+
"Noticed On": "2026-09-29",
|
|
1503
|
+
"Noticed At": "2026-09-29T08:15:00.000Z",
|
|
1504
|
+
Untyped: "free text",
|
|
1505
|
+
"Customer Facing": false,
|
|
1506
|
+
}),
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
expect(value.customFields).toEqual({
|
|
1510
|
+
...REQUIRED_ANSWERS,
|
|
1511
|
+
"Users Affected": "1200",
|
|
1512
|
+
Region: "EU",
|
|
1513
|
+
Systems: ["Web", "API"],
|
|
1514
|
+
"Noticed On": "2026-09-29",
|
|
1515
|
+
"Noticed At": "2026-09-29T08:15:00.000Z",
|
|
1516
|
+
Untyped: "free text",
|
|
1517
|
+
"Customer Facing": false,
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
test("a number may be sent as a number", () => {
|
|
1522
|
+
expect(
|
|
1523
|
+
valueOf(answer({ "Users Affected": 0 })).customFields["Users Affected"],
|
|
1524
|
+
).toBe(0);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
test("a required yes/no must be ticked: no, missing, or anything else is refused", () => {
|
|
1528
|
+
const name: string = "I Have Checked The Status Page";
|
|
1529
|
+
|
|
1530
|
+
for (const unticked of [false, "false", null, ""]) {
|
|
1531
|
+
expect(
|
|
1532
|
+
errorsOf(
|
|
1533
|
+
validate(
|
|
1534
|
+
submission({
|
|
1535
|
+
customFields: {
|
|
1536
|
+
Impact: "Down",
|
|
1537
|
+
[name]: unticked as JSONObject[string],
|
|
1538
|
+
},
|
|
1539
|
+
}),
|
|
1540
|
+
),
|
|
1541
|
+
),
|
|
1542
|
+
).toEqual([`${name} must be checked.`]);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
expect(
|
|
1546
|
+
errorsOf(validate(submission({ customFields: { Impact: "Down" } }))),
|
|
1547
|
+
).toEqual([`${name} must be checked.`]);
|
|
1548
|
+
|
|
1549
|
+
expect(
|
|
1550
|
+
errorsOf(
|
|
1551
|
+
validate(
|
|
1552
|
+
submission({ customFields: { Impact: "Down", [name]: "yes" } }),
|
|
1553
|
+
),
|
|
1554
|
+
),
|
|
1555
|
+
).toEqual([`"${name}" holds true or false, but was sent "yes".`]);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
test('a yes/no answered "true" or "false" is stored as the boolean the dashboard stores', () => {
|
|
1559
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1560
|
+
validate(
|
|
1561
|
+
submission({
|
|
1562
|
+
customFields: {
|
|
1563
|
+
Impact: "Down",
|
|
1564
|
+
"I Have Checked The Status Page": " true ",
|
|
1565
|
+
"Customer Facing": "false",
|
|
1566
|
+
},
|
|
1567
|
+
}),
|
|
1568
|
+
),
|
|
1569
|
+
);
|
|
1570
|
+
|
|
1571
|
+
expect(value.customFields["I Have Checked The Status Page"]).toBe(true);
|
|
1572
|
+
expect(value.customFields["Customer Facing"]).toBe(false);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test("a multi-select holds each option once, trimmed, and one bare option becomes a list", () => {
|
|
1576
|
+
expect(
|
|
1577
|
+
valueOf(answer({ Systems: ["API", " API ", "", "Web", "API"] }))
|
|
1578
|
+
.customFields["Systems"],
|
|
1579
|
+
).toEqual(["API", "Web"]);
|
|
1580
|
+
|
|
1581
|
+
expect(
|
|
1582
|
+
valueOf(answer({ Systems: " Web " })).customFields["Systems"],
|
|
1583
|
+
).toEqual(["Web"]);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
test("a required multi-select with nothing chosen is unanswered", () => {
|
|
1587
|
+
expect(
|
|
1588
|
+
errorsOf(
|
|
1589
|
+
validate(
|
|
1590
|
+
submission({
|
|
1591
|
+
customFields: { ...REQUIRED_ANSWERS, Systems: ["", " "] },
|
|
1592
|
+
}),
|
|
1593
|
+
{
|
|
1594
|
+
askedDefinitions: [
|
|
1595
|
+
...ASKED.filter((field: IncidentFormAskedDefinition): boolean => {
|
|
1596
|
+
return field.name !== "Systems";
|
|
1597
|
+
}),
|
|
1598
|
+
{
|
|
1599
|
+
name: "Systems",
|
|
1600
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
1601
|
+
dropdownOptions: "API\nWeb",
|
|
1602
|
+
isRequiredOnCreate: true,
|
|
1603
|
+
},
|
|
1604
|
+
],
|
|
1605
|
+
},
|
|
1606
|
+
),
|
|
1607
|
+
),
|
|
1608
|
+
).toEqual(["Systems is required."]);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
test("text answers may be 10000 characters, not 10001 - a list entry included", () => {
|
|
1612
|
+
const longest: string = "x".repeat(
|
|
1613
|
+
INCIDENT_FORM_CUSTOM_FIELD_TEXT_MAX_LENGTH,
|
|
1614
|
+
);
|
|
1615
|
+
|
|
1616
|
+
expect(valueOf(answer({ Notes: longest })).customFields["Notes"]).toBe(
|
|
1617
|
+
longest,
|
|
1618
|
+
);
|
|
1619
|
+
|
|
1620
|
+
expect(errorsOf(answer({ Notes: `${longest}x` }))).toEqual([
|
|
1621
|
+
"Notes cannot be more than 10000 characters.",
|
|
1622
|
+
]);
|
|
1623
|
+
expect(
|
|
1624
|
+
errorsOf(answer({ "Users Affected": `1${"0".repeat(10000)}` })),
|
|
1625
|
+
).toEqual(["Users Affected cannot be more than 10000 characters."]);
|
|
1626
|
+
expect(errorsOf(answer({ Systems: ["API", `${longest}x`] }))).toEqual([
|
|
1627
|
+
"Systems cannot be more than 10000 characters.",
|
|
1628
|
+
]);
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
/*
|
|
1632
|
+
* A multi-select answer is bounded before any of its entries is read:
|
|
1633
|
+
* otherwise a single request of a million entries has each of them
|
|
1634
|
+
* cleaned, sorted and quoted back in a refusal as large as the request,
|
|
1635
|
+
* or - for a field nobody gave options to - stored on the incident.
|
|
1636
|
+
*/
|
|
1637
|
+
describe("how many choices a multi-select answer may have", () => {
|
|
1638
|
+
const systems: (options: string) => Array<IncidentFormAskedDefinition> = (
|
|
1639
|
+
options: string,
|
|
1640
|
+
): Array<IncidentFormAskedDefinition> => {
|
|
1641
|
+
return [
|
|
1642
|
+
{
|
|
1643
|
+
name: "Systems",
|
|
1644
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
1645
|
+
dropdownOptions: options,
|
|
1646
|
+
isRequiredOnCreate: false,
|
|
1647
|
+
},
|
|
1648
|
+
];
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const entries: (count: number, prefix?: string) => Array<string> = (
|
|
1652
|
+
count: number,
|
|
1653
|
+
prefix: string = "x",
|
|
1654
|
+
): Array<string> => {
|
|
1655
|
+
return Array.from({ length: count }, (_value: unknown, index: number) => {
|
|
1656
|
+
return `${prefix}${index}`;
|
|
1657
|
+
});
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
test("is at most 100, whatever is in the list, and the refusal quotes none of it", () => {
|
|
1661
|
+
const errors: Array<string> = errorsOf(
|
|
1662
|
+
validate(submission({ customFields: { Systems: entries(101) } }), {
|
|
1663
|
+
askedDefinitions: systems("EU\nUS\nAPAC"),
|
|
1664
|
+
}),
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
expect(errors).toEqual([
|
|
1668
|
+
`Systems cannot have more than ${INCIDENT_FORM_MULTI_SELECT_MAX_CHOICES} choices.`,
|
|
1669
|
+
]);
|
|
1670
|
+
expect(INCIDENT_FORM_MULTI_SELECT_MAX_CHOICES).toBe(100);
|
|
1671
|
+
expect(errors.join(" ")).not.toContain("x0");
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
test("is counted before duplicates are dropped", () => {
|
|
1675
|
+
expect(
|
|
1676
|
+
errorsOf(
|
|
1677
|
+
validate(
|
|
1678
|
+
submission({
|
|
1679
|
+
customFields: {
|
|
1680
|
+
Systems: Array(101).fill("EU") as JSONObject[string],
|
|
1681
|
+
},
|
|
1682
|
+
}),
|
|
1683
|
+
{ askedDefinitions: systems("EU\nUS") },
|
|
1684
|
+
),
|
|
1685
|
+
),
|
|
1686
|
+
).toEqual(["Systems cannot have more than 100 choices."]);
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
test("refuses a million entries at once, quickly, with one short message", () => {
|
|
1690
|
+
const started: number = Date.now();
|
|
1691
|
+
|
|
1692
|
+
const errors: Array<string> = errorsOf(
|
|
1693
|
+
validate(
|
|
1694
|
+
submission({ customFields: { Systems: entries(1_000_000) } }),
|
|
1695
|
+
{ askedDefinitions: systems("EU\nUS\nAPAC") },
|
|
1696
|
+
),
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
expect(Date.now() - started).toBeLessThan(1000);
|
|
1700
|
+
expect(errors).toEqual(["Systems cannot have more than 100 choices."]);
|
|
1701
|
+
expect(formatIncidentFormSubmissionErrors(errors).length).toBeLessThan(
|
|
1702
|
+
200,
|
|
1703
|
+
);
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
test("on a field nobody gave options to, 100 entries are stored and 101 refused", () => {
|
|
1707
|
+
const hundred: Array<string> = entries(100);
|
|
1708
|
+
|
|
1709
|
+
expect(
|
|
1710
|
+
valueOf(
|
|
1711
|
+
validate(submission({ customFields: { Systems: hundred } }), {
|
|
1712
|
+
askedDefinitions: systems(""),
|
|
1713
|
+
}),
|
|
1714
|
+
).customFields["Systems"],
|
|
1715
|
+
).toEqual(hundred);
|
|
1716
|
+
|
|
1717
|
+
const refused: IncidentFormSubmissionValidationResult = validate(
|
|
1718
|
+
submission({ customFields: { Systems: entries(101) } }),
|
|
1719
|
+
{ askedDefinitions: systems("") },
|
|
1720
|
+
);
|
|
1721
|
+
|
|
1722
|
+
expect(errorsOf(refused)).toEqual([
|
|
1723
|
+
"Systems cannot have more than 100 choices.",
|
|
1724
|
+
]);
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
test("is the field's number of options, when it has more than 100 - so every option can be chosen", () => {
|
|
1728
|
+
const options: Array<string> = entries(150, "option ");
|
|
1729
|
+
|
|
1730
|
+
expect(
|
|
1731
|
+
valueOf(
|
|
1732
|
+
validate(submission({ customFields: { Systems: options } }), {
|
|
1733
|
+
askedDefinitions: systems(options.join("\n")),
|
|
1734
|
+
}),
|
|
1735
|
+
).customFields["Systems"],
|
|
1736
|
+
).toEqual(options);
|
|
1737
|
+
|
|
1738
|
+
expect(
|
|
1739
|
+
errorsOf(
|
|
1740
|
+
validate(
|
|
1741
|
+
submission({
|
|
1742
|
+
customFields: { Systems: [...options, "option 0"] },
|
|
1743
|
+
}),
|
|
1744
|
+
{ askedDefinitions: systems(options.join("\n")) },
|
|
1745
|
+
),
|
|
1746
|
+
),
|
|
1747
|
+
).toEqual(["Systems cannot have more than 150 choices."]);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
test.each([
|
|
1751
|
+
["a text question", "Impact"],
|
|
1752
|
+
["a long text question", "Steps to Reproduce"],
|
|
1753
|
+
["a Markdown question", "Notes"],
|
|
1754
|
+
["a number question", "Users Affected"],
|
|
1755
|
+
["a yes/no question", "Customer Facing"],
|
|
1756
|
+
["a dropdown", "Region"],
|
|
1757
|
+
["a date question", "Noticed On"],
|
|
1758
|
+
["a question with no type", "Untyped"],
|
|
1759
|
+
])(
|
|
1760
|
+
"refuses a list for %s, without reading it",
|
|
1761
|
+
(_label: string, name: string) => {
|
|
1762
|
+
const errors: Array<string> = errorsOf(
|
|
1763
|
+
answer({ [name]: entries(1_000_000) }),
|
|
1764
|
+
);
|
|
1765
|
+
|
|
1766
|
+
expect(errors).toEqual([`${name} takes one answer, not a list.`]);
|
|
1767
|
+
},
|
|
1768
|
+
);
|
|
1769
|
+
|
|
1770
|
+
test("an empty list for a single-answer question is still no answer", () => {
|
|
1771
|
+
expect(errorsOf(answer({ Impact: [] }))).toEqual(["Impact is required."]);
|
|
1772
|
+
expect(
|
|
1773
|
+
valueOf(answer({ Region: [] })).customFields["Region"],
|
|
1774
|
+
).toBeUndefined();
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
/*
|
|
1778
|
+
* A choice is one option. A list nested inside the answer - thousands
|
|
1779
|
+
* deep, which the app's JSON parser accepts - would overflow the stack
|
|
1780
|
+
* the moment anything turned it into text, so a list or an object
|
|
1781
|
+
* inside the list is refused unread, and validation still never throws.
|
|
1782
|
+
*/
|
|
1783
|
+
test.each([
|
|
1784
|
+
["a list inside the list", [["EU"]]],
|
|
1785
|
+
["an object inside the list", [{ EU: true }]],
|
|
1786
|
+
["a choice beside a list", ["EU", ["US"]]],
|
|
1787
|
+
])("refuses %s", (_label: string, list: Array<unknown>) => {
|
|
1788
|
+
expect(
|
|
1789
|
+
errorsOf(
|
|
1790
|
+
validate(
|
|
1791
|
+
submission({
|
|
1792
|
+
customFields: { Systems: list as JSONObject[string] },
|
|
1793
|
+
}),
|
|
1794
|
+
{ askedDefinitions: systems("EU\nUS\nAPAC") },
|
|
1795
|
+
),
|
|
1796
|
+
),
|
|
1797
|
+
).toEqual([
|
|
1798
|
+
"Systems takes a list of its options, not lists or objects within it.",
|
|
1799
|
+
]);
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
test("refuses a list nested a hundred thousand deep, without throwing", () => {
|
|
1803
|
+
let nested: unknown = "EU";
|
|
1804
|
+
|
|
1805
|
+
for (let depth: number = 0; depth < 100000; depth++) {
|
|
1806
|
+
nested = [nested];
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
let result: IncidentFormSubmissionValidationResult | undefined;
|
|
1810
|
+
|
|
1811
|
+
expect(() => {
|
|
1812
|
+
result = validate(
|
|
1813
|
+
submission({
|
|
1814
|
+
customFields: { Systems: [nested] as JSONObject[string] },
|
|
1815
|
+
}),
|
|
1816
|
+
{ askedDefinitions: systems("EU\nUS\nAPAC") },
|
|
1817
|
+
);
|
|
1818
|
+
}).not.toThrow();
|
|
1819
|
+
|
|
1820
|
+
expect(errorsOf(result!)).toEqual([
|
|
1821
|
+
"Systems takes a list of its options, not lists or objects within it.",
|
|
1822
|
+
]);
|
|
1823
|
+
});
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
test("a field name with $ patterns is quoted back as typed", () => {
|
|
1827
|
+
expect(
|
|
1828
|
+
errorsOf(
|
|
1829
|
+
validate(submission({ customFields: {} }), {
|
|
1830
|
+
askedDefinitions: [
|
|
1831
|
+
{
|
|
1832
|
+
name: "Cost $& ($1) $$",
|
|
1833
|
+
customFieldType: CustomFieldType.Text,
|
|
1834
|
+
isRequiredOnCreate: true,
|
|
1835
|
+
},
|
|
1836
|
+
],
|
|
1837
|
+
}),
|
|
1838
|
+
),
|
|
1839
|
+
).toEqual(["Cost $& ($1) $$ is required."]);
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
test("regression: a field named __proto__ is stored as an answer, not as the object's prototype", () => {
|
|
1843
|
+
const asked: Array<IncidentFormAskedDefinition> = [
|
|
1844
|
+
{
|
|
1845
|
+
name: "__proto__",
|
|
1846
|
+
customFieldType: CustomFieldType.MultiSelectDropdown,
|
|
1847
|
+
dropdownOptions: "API\nWeb",
|
|
1848
|
+
isRequiredOnCreate: true,
|
|
1849
|
+
},
|
|
1850
|
+
];
|
|
1851
|
+
|
|
1852
|
+
const value: ValidatedIncidentFormSubmission = valueOf(
|
|
1853
|
+
validate(
|
|
1854
|
+
submission({
|
|
1855
|
+
customFields: JSON.parse('{"__proto__": ["API"]}') as JSONObject,
|
|
1856
|
+
}),
|
|
1857
|
+
{ askedDefinitions: asked },
|
|
1858
|
+
),
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
expect(Object.getPrototypeOf(value.customFields)).toBe(Object.prototype);
|
|
1862
|
+
expect(Object.keys(value.customFields)).toEqual(["__proto__"]);
|
|
1863
|
+
expect(JSON.stringify(value.customFields)).toBe('{"__proto__":["API"]}');
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
test("an inherited answer is not an answer", () => {
|
|
1867
|
+
const answers: JSONObject = Object.create({
|
|
1868
|
+
Impact: "Inherited",
|
|
1869
|
+
}) as JSONObject;
|
|
1870
|
+
|
|
1871
|
+
expect(errorsOf(validate(submission({ customFields: answers })))).toEqual([
|
|
1872
|
+
"Impact is required.",
|
|
1873
|
+
"I Have Checked The Status Page must be checked.",
|
|
1874
|
+
]);
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
describe("formatIncidentFormSubmissionErrors", () => {
|
|
1879
|
+
test("joins every problem into one message", () => {
|
|
1880
|
+
expect(
|
|
1881
|
+
formatIncidentFormSubmissionErrors([
|
|
1882
|
+
"Title is required.",
|
|
1883
|
+
"Your Email is not a valid email address.",
|
|
1884
|
+
]),
|
|
1885
|
+
).toBe("Title is required. Your Email is not a valid email address.");
|
|
1886
|
+
expect(formatIncidentFormSubmissionErrors([])).toBe("");
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
// A relative import: another pure module of Common.
|
|
1891
|
+
const RELATIVE_IMPORT: RegExp = /^\.\.?\//;
|
|
1892
|
+
|
|
1893
|
+
describe("the module stays pure", () => {
|
|
1894
|
+
test("imports nothing from React, the database, the server or the UI", () => {
|
|
1895
|
+
const source: string = fs.readFileSync(
|
|
1896
|
+
path.join(__dirname, "../../../Types/Incident/IncidentFormPublic.ts"),
|
|
1897
|
+
"utf8",
|
|
1898
|
+
);
|
|
1899
|
+
|
|
1900
|
+
const imports: Array<string> = Array.from(
|
|
1901
|
+
source.matchAll(/from\s+"([^"]+)"/g),
|
|
1902
|
+
).map((match: RegExpMatchArray): string => {
|
|
1903
|
+
return match[1]!;
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
expect(imports.length).toBeGreaterThan(0);
|
|
1907
|
+
|
|
1908
|
+
for (const imported of imports) {
|
|
1909
|
+
expect({ imported, relative: RELATIVE_IMPORT.test(imported) }).toEqual({
|
|
1910
|
+
imported,
|
|
1911
|
+
relative: true,
|
|
1912
|
+
});
|
|
1913
|
+
expect(imported).not.toMatch(/react|typeorm|Server|UI|Models/i);
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
});
|