@oneuptime/common 12.0.22 → 12.0.24
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/DetectionRule.ts +55 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +584 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +105 -0
- package/Models/DatabaseModels/Project.ts +68 -0
- package/Models/DatabaseModels/User.ts +68 -0
- package/Server/API/TelemetryAPI.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Middleware/GzipRequestBody.ts +311 -0
- package/Server/Middleware/MultipartFormData.ts +87 -3
- package/Server/Services/DetectionRuleService.ts +73 -6
- package/Server/Services/Index.ts +207 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
- package/Server/Services/NetworkDeviceService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
- package/Server/Services/ProjectService.ts +29 -19
- package/Server/Services/SecurityEventService.ts +90 -13
- package/Server/Services/StatusPageResourceService.ts +223 -0
- package/Server/Services/TelemetryAttributeService.ts +17 -0
- package/Server/Services/UserService.ts +4 -6
- package/Server/Types/Markdown.ts +161 -67
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
- package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
- package/Server/Utils/Attribution.ts +4 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
- package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
- package/Server/Utils/StartServer.ts +12 -32
- package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
- package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
- package/Server/Views/Partials/Head.ejs +2 -1
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
- package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
- package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
- package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
- package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
- package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
- package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
- package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
- package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
- package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
- package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
- package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
- package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
- package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
- package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
- package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
- package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
- package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
- package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
- package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
- package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
- package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
- package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
- package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
- package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
- package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
- package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
- package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
- package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
- package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
- package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
- package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
- package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
- package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
- package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
- package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
- package/Tests/UI/Components/SideOver.test.tsx +1 -1
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
- package/Tests/UI/Components/Toggle.test.tsx +25 -0
- package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
- package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
- package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
- package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
- package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
- package/Types/Code/YamlSyntax.ts +396 -0
- package/Types/Exception/ExceptionCode.ts +1 -0
- package/Types/Exception/PayloadTooLargeException.ts +8 -0
- package/Types/Marketing/Attribution.ts +10 -0
- package/Types/Marketing/MarketingEvent.ts +4 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
- package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
- package/Types/Permission.ts +47 -0
- package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
- package/Types/Telemetry/LlmMetricConventions.ts +124 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
- package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
- package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
- package/UI/Components/Detail/Detail.tsx +10 -0
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
- package/UI/Components/Forms/Fields/FormField.tsx +31 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
- package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
- package/UI/Components/Forms/Validation.ts +46 -0
- package/UI/Components/Input/Input.tsx +3 -0
- package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
- package/UI/Components/ModelTable/Column.ts +8 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
- package/UI/Components/SideOver/SideOver.tsx +5 -5
- package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
- package/UI/Components/StatusPage/ResourceList.tsx +197 -38
- package/UI/Components/Toggle/Toggle.tsx +9 -1
- package/UI/Components/Types/FieldType.ts +1 -0
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
- package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
- package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
- package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
- package/Utils/StatusPage/ResourceExplorer.ts +258 -0
- package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +609 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +97 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +76 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +76 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +17 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
- package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
- package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
- package/build/dist/Server/Services/DetectionRuleService.js +49 -4
- package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +206 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -45
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventService.js +57 -5
- package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
- package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +2 -12
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +147 -61
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
- package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -21
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
- package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
- package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
- package/build/dist/Types/Code/YamlSyntax.js +212 -0
- package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
- package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
- package/build/dist/Types/Marketing/Attribution.js +10 -0
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/Permission.js +41 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
- package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
- package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
- package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +9 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
- package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
- package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +27 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
- package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
- package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Types/FieldType.js +1 -0
- package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
- package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
- package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
- package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/Tests/UI/Components/Toast.test.tsx +0 -197
- package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
- package/UI/Components/Toast/Toast.tsx +0 -189
- package/UI/Components/Toast/ToastInit.tsx +0 -110
- package/build/dist/UI/Components/Toast/Toast.js +0 -123
- package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
- package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
waitFor,
|
|
16
16
|
within,
|
|
17
17
|
} from "@testing-library/react";
|
|
18
|
-
import React from "react";
|
|
18
|
+
import React, { act } from "react";
|
|
19
19
|
import {
|
|
20
20
|
MemoryRouter,
|
|
21
21
|
Outlet,
|
|
@@ -361,6 +361,40 @@ const respondWithMethods: RespondWithMethodsFunction = (
|
|
|
361
361
|
});
|
|
362
362
|
};
|
|
363
363
|
|
|
364
|
+
type RespondWithDeletionImpactFunction = (impact: JSONObject) => void;
|
|
365
|
+
|
|
366
|
+
/*
|
|
367
|
+
* The impact endpoint hangs off the same base route as the list, so its url
|
|
368
|
+
* CONTAINS the list's url. It is matched first for that reason: the other way
|
|
369
|
+
* round, every impact read is answered with the method list and the
|
|
370
|
+
* confirmation draws a preview out of a payload that has no counts in it.
|
|
371
|
+
*/
|
|
372
|
+
const respondWithDeletionImpact: RespondWithDeletionImpactFunction = (
|
|
373
|
+
impact: JSONObject,
|
|
374
|
+
): void => {
|
|
375
|
+
apiGetMock.mockImplementation((data: any) => {
|
|
376
|
+
const url: string = String(data.url);
|
|
377
|
+
|
|
378
|
+
if (url.includes("/deletion-impact")) {
|
|
379
|
+
return Promise.resolve(new HTTPResponse<JSONObject>(200, impact, {}));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (url.includes("/user-notification-method-admin/")) {
|
|
383
|
+
return Promise.resolve(
|
|
384
|
+
new HTTPResponse<JSONObject>(
|
|
385
|
+
200,
|
|
386
|
+
{ methods: [VERIFIED_EMAIL, UNVERIFIED_SMS, WEBHOOK] },
|
|
387
|
+
{},
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return Promise.resolve(
|
|
393
|
+
new HTTPResponse<JSONObject>(200, READINESS_PAYLOAD, {}),
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
|
|
364
398
|
type BuildTeamMemberFunction = () => TeamMember;
|
|
365
399
|
|
|
366
400
|
const buildTeamMember: BuildTeamMemberFunction = (): TeamMember => {
|
|
@@ -375,6 +409,96 @@ const buildTeamMember: BuildTeamMemberFunction = (): TeamMember => {
|
|
|
375
409
|
return member;
|
|
376
410
|
};
|
|
377
411
|
|
|
412
|
+
type FlushEffectsFunction = () => Promise<void>;
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* One turn of the macrotask queue, inside `act`.
|
|
416
|
+
*
|
|
417
|
+
* That is what it takes for an effect scheduled by the commit that just
|
|
418
|
+
* happened to run, and for the render THAT effect schedules to commit in turn.
|
|
419
|
+
* Awaiting a promise is not enough: React 18 schedules its work through the
|
|
420
|
+
* scheduler rather than through the microtask queue, so a state update made
|
|
421
|
+
* from an effect is still uncommitted when the next microtask runs.
|
|
422
|
+
*/
|
|
423
|
+
const flushEffects: FlushEffectsFunction = async (): Promise<void> => {
|
|
424
|
+
await act(async (): Promise<void> => {
|
|
425
|
+
await new Promise<void>((resolve: () => void): void => {
|
|
426
|
+
setTimeout(resolve, 0);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
type IsSettledFunction = () => boolean;
|
|
432
|
+
|
|
433
|
+
/*
|
|
434
|
+
* Three things, and none of them is sufficient alone.
|
|
435
|
+
*
|
|
436
|
+
* `pendingRequestCount` dips to zero between the SECTION layout's reads
|
|
437
|
+
* finishing and the page's own read being issued — the page does not exist
|
|
438
|
+
* until the layout has an identity to render it with, so waiting on the counter
|
|
439
|
+
* alone hands the test a loading skeleton. Worse, the counter goes back to zero
|
|
440
|
+
* one turn BEFORE the awaiting component sets any state: the tracking `finally`
|
|
441
|
+
* is registered on the response promise ahead of the component's own
|
|
442
|
+
* continuation, so a poll landing in that turn sees "nothing in flight" while
|
|
443
|
+
* the section is still drawing its PageLoader.
|
|
444
|
+
*
|
|
445
|
+
* The page's own skeleton is no better on its own: it is absent before the
|
|
446
|
+
* first render, and the SELF branch never renders one at all.
|
|
447
|
+
*
|
|
448
|
+
* So the loaders are named explicitly — `bar-loader` is PageLoader's, and it is
|
|
449
|
+
* the section still deciding who this page is about.
|
|
450
|
+
*/
|
|
451
|
+
const isSettled: IsSettledFunction = (): boolean => {
|
|
452
|
+
return (
|
|
453
|
+
pendingRequestCount === 0 &&
|
|
454
|
+
screen.queryByTestId("methods-loading") === null &&
|
|
455
|
+
screen.queryByTestId("bar-loader") === null
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
type AssertSettledFunction = () => void;
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
* The same three conditions as assertions, so a wait that runs out says WHICH
|
|
463
|
+
* of them never came true rather than "timed out".
|
|
464
|
+
*/
|
|
465
|
+
const assertSettled: AssertSettledFunction = (): void => {
|
|
466
|
+
expect(pendingRequestCount).toBe(0);
|
|
467
|
+
expect(screen.queryByTestId("methods-loading")).not.toBeInTheDocument();
|
|
468
|
+
expect(screen.queryByTestId("bar-loader")).not.toBeInTheDocument();
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
type WaitForSettledFunction = () => Promise<void>;
|
|
472
|
+
|
|
473
|
+
/*
|
|
474
|
+
* Settled, and STILL settled once everything that commit scheduled has run.
|
|
475
|
+
*
|
|
476
|
+
* The recheck is the part that matters, and the self view is why. It issues no
|
|
477
|
+
* request of its own, so the moment the layout's reads finish, every condition
|
|
478
|
+
* above holds — while the tab strip it just rendered has an empty panel, because
|
|
479
|
+
* Tabs picks its opening tab in an effect. A single `waitFor` returns on that
|
|
480
|
+
* frame and the test reads a page that has not finished appearing. Flushing and
|
|
481
|
+
* asking again is what turns "no longer busy" into "done".
|
|
482
|
+
*
|
|
483
|
+
* Looping rather than flushing once, because a flush can also START work — the
|
|
484
|
+
* commit it releases is the one that mounts the page that issues the read.
|
|
485
|
+
*/
|
|
486
|
+
const waitForSettled: WaitForSettledFunction = async (): Promise<void> => {
|
|
487
|
+
for (let attempt: number = 0; attempt < 25; attempt++) {
|
|
488
|
+
await waitFor(assertSettled, { timeout: 4000 });
|
|
489
|
+
|
|
490
|
+
await flushEffects();
|
|
491
|
+
|
|
492
|
+
if (isSettled()) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
throw new Error(
|
|
498
|
+
"the page never stopped loading: it kept scheduling more work after every flush",
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
|
|
378
502
|
type RenderPageFunction = () => Promise<HTMLElement>;
|
|
379
503
|
|
|
380
504
|
const renderPage: RenderPageFunction = async (): Promise<HTMLElement> => {
|
|
@@ -400,26 +524,75 @@ const renderPage: RenderPageFunction = async (): Promise<HTMLElement> => {
|
|
|
400
524
|
</MemoryRouter>,
|
|
401
525
|
);
|
|
402
526
|
|
|
403
|
-
|
|
404
|
-
* Two conditions, and neither is sufficient alone.
|
|
405
|
-
*
|
|
406
|
-
* `pendingRequestCount` dips to zero between the SECTION layout's reads
|
|
407
|
-
* finishing and the page's own read being issued — the page does not exist
|
|
408
|
-
* until the layout has an identity to render it with, so waiting on the
|
|
409
|
-
* counter alone hands the test a loading skeleton. The skeleton's absence
|
|
410
|
-
* alone is no better: it is also absent before the first render.
|
|
411
|
-
*/
|
|
412
|
-
await waitFor(
|
|
413
|
-
(): void => {
|
|
414
|
-
expect(pendingRequestCount).toBe(0);
|
|
415
|
-
expect(screen.queryByTestId("methods-loading")).not.toBeInTheDocument();
|
|
416
|
-
},
|
|
417
|
-
{ timeout: 4000 },
|
|
418
|
-
);
|
|
527
|
+
await waitForSettled();
|
|
419
528
|
|
|
420
529
|
return container;
|
|
421
530
|
};
|
|
422
531
|
|
|
532
|
+
type RenderSelfPageFunction = () => Promise<HTMLElement>;
|
|
533
|
+
|
|
534
|
+
/*
|
|
535
|
+
* The same route, read by the person it is about. Everything else — the
|
|
536
|
+
* permissions, the fixtures, the project — stays exactly as the admin case
|
|
537
|
+
* leaves it, so what these assertions read is the isSelf branch and not some
|
|
538
|
+
* other difference smuggled in beside it.
|
|
539
|
+
*/
|
|
540
|
+
const renderSelfPage: RenderSelfPageFunction =
|
|
541
|
+
async (): Promise<HTMLElement> => {
|
|
542
|
+
jest
|
|
543
|
+
.spyOn(UserUtil, "getUserId")
|
|
544
|
+
.mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
|
|
545
|
+
|
|
546
|
+
return renderPage();
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
type OpenTabFunction = (name: string) => Promise<void>;
|
|
550
|
+
|
|
551
|
+
const openTab: OpenTabFunction = async (name: string): Promise<void> => {
|
|
552
|
+
fireEvent.click(screen.getByTestId(`tab-${name}`));
|
|
553
|
+
|
|
554
|
+
await waitForSettled();
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
type ChannelOptionsFunction = () => Array<string>;
|
|
558
|
+
|
|
559
|
+
/*
|
|
560
|
+
* The channel dropdown is a react-select, not a <select>.
|
|
561
|
+
*
|
|
562
|
+
* It renders no <option> elements at all until its menu is opened, so a
|
|
563
|
+
* querySelectorAll("option") over the closed one answers "no options" for a
|
|
564
|
+
* dropdown with four — and every "that channel is not offered" assertion
|
|
565
|
+
* written against that answer passes without having looked at anything.
|
|
566
|
+
*/
|
|
567
|
+
const channelOptions: ChannelOptionsFunction = (): Array<string> => {
|
|
568
|
+
const combobox: HTMLElement | null =
|
|
569
|
+
document.querySelector<HTMLElement>("[role=combobox]");
|
|
570
|
+
|
|
571
|
+
if (!combobox) {
|
|
572
|
+
throw new Error("the add form has no channel dropdown");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
fireEvent.keyDown(combobox, { key: "ArrowDown", code: 40, keyCode: 40 });
|
|
576
|
+
|
|
577
|
+
return screen.queryAllByRole("option").map((option: HTMLElement): string => {
|
|
578
|
+
return option.textContent || "";
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
type ChooseChannelFunction = (label: string) => void;
|
|
583
|
+
|
|
584
|
+
const chooseChannel: ChooseChannelFunction = (label: string): void => {
|
|
585
|
+
const offered: Array<string> = channelOptions();
|
|
586
|
+
|
|
587
|
+
if (!offered.includes(label)) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`the add form does not offer ${label}; it offers ${offered.join(", ")}`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
fireEvent.click(screen.getByRole("option", { name: label }));
|
|
594
|
+
};
|
|
595
|
+
|
|
423
596
|
type MethodRowsFunction = () => Array<HTMLElement>;
|
|
424
597
|
|
|
425
598
|
const methodRows: MethodRowsFunction = (): Array<HTMLElement> => {
|
|
@@ -667,17 +840,14 @@ describe("adding a method on somebody's behalf", () => {
|
|
|
667
840
|
* at all, so an admin-created one would be live the instant it was
|
|
668
841
|
* written, which is exactly the silent redirect this design rules out.
|
|
669
842
|
*/
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
843
|
+
/*
|
|
844
|
+
* Read out of the OPEN menu. The page copy under the list mentions WhatsApp
|
|
845
|
+
* and the rows above it mention SMS, so a scan of document.body says
|
|
846
|
+
* "offered" about a channel this form has never heard of.
|
|
847
|
+
*/
|
|
848
|
+
const optionLabels: Array<string> = channelOptions();
|
|
675
849
|
|
|
676
|
-
|
|
677
|
-
document.querySelectorAll("option"),
|
|
678
|
-
).map((option: Element): string => {
|
|
679
|
-
return option.textContent || "";
|
|
680
|
-
});
|
|
850
|
+
expect(optionLabels).toEqual(["Email", "SMS", "Phone call", "WhatsApp"]);
|
|
681
851
|
|
|
682
852
|
for (const forbidden of [
|
|
683
853
|
"Push",
|
|
@@ -745,11 +915,7 @@ describe("adding a method on somebody's behalf", () => {
|
|
|
745
915
|
|
|
746
916
|
fireEvent.change(valueInput, { target: { value: RAW_PHONE } });
|
|
747
917
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (methodSelect) {
|
|
751
|
-
fireEvent.change(methodSelect, { target: { value: "SMS" } });
|
|
752
|
-
}
|
|
918
|
+
chooseChannel("SMS");
|
|
753
919
|
|
|
754
920
|
fireEvent.click(within(modal()).getByText("Add"));
|
|
755
921
|
|
|
@@ -769,6 +935,14 @@ describe("adding a method on somebody's behalf", () => {
|
|
|
769
935
|
`/user-notification-method-admin/user/${TARGET_USER_ID_STRING}`,
|
|
770
936
|
);
|
|
771
937
|
expect(call.data.value).toBe(RAW_PHONE);
|
|
938
|
+
|
|
939
|
+
/*
|
|
940
|
+
* And the channel the admin picked, not the one the form opened on. The
|
|
941
|
+
* previous spelling of this reached for a <select> that a react-select
|
|
942
|
+
* dropdown never renders, found nothing, and asserted the value of a form
|
|
943
|
+
* still sitting on its default.
|
|
944
|
+
*/
|
|
945
|
+
expect(call.data.methodType).toBe("SMS");
|
|
772
946
|
});
|
|
773
947
|
|
|
774
948
|
test("re-reads the list and the readiness summary after a successful add", async () => {
|
|
@@ -963,9 +1137,7 @@ describe("removing a method", () => {
|
|
|
963
1137
|
|
|
964
1138
|
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
965
1139
|
|
|
966
|
-
await
|
|
967
|
-
expect(pendingRequestCount).toBe(0);
|
|
968
|
-
});
|
|
1140
|
+
await waitForSettled();
|
|
969
1141
|
|
|
970
1142
|
/*
|
|
971
1143
|
* A preview is an improvement on the confirmation, not a precondition for
|
|
@@ -1147,9 +1319,7 @@ describe("no unmasked identifier reaches the DOM", () => {
|
|
|
1147
1319
|
|
|
1148
1320
|
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
1149
1321
|
|
|
1150
|
-
await
|
|
1151
|
-
expect(pendingRequestCount).toBe(0);
|
|
1152
|
-
});
|
|
1322
|
+
await waitForSettled();
|
|
1153
1323
|
|
|
1154
1324
|
expect(document.body.textContent).toContain(MASKED_EMAIL);
|
|
1155
1325
|
|
|
@@ -1160,3 +1330,571 @@ describe("no unmasked identifier reaches the DOM", () => {
|
|
|
1160
1330
|
}
|
|
1161
1331
|
});
|
|
1162
1332
|
});
|
|
1333
|
+
|
|
1334
|
+
/*
|
|
1335
|
+
* The self view, in the depth the one test above it does not go to.
|
|
1336
|
+
*
|
|
1337
|
+
* This branch is the reason the masked admin list is allowed to be as narrow as
|
|
1338
|
+
* it is: everything an administrator cannot do here — read an identifier, enter
|
|
1339
|
+
* a verification code, set up a push device — the owner can do on the very same
|
|
1340
|
+
* route, because for them the method models are their own rows. A self view
|
|
1341
|
+
* that quietly rendered nothing would look exactly like a self view that
|
|
1342
|
+
* worked, right up until somebody needed to add a phone number.
|
|
1343
|
+
*/
|
|
1344
|
+
describe("the self-serve view", () => {
|
|
1345
|
+
test("reaches every one of the nine method models across its tabs", async () => {
|
|
1346
|
+
await renderSelfPage();
|
|
1347
|
+
|
|
1348
|
+
/*
|
|
1349
|
+
* Direct Contact is the opening tab, and the five channels a person is
|
|
1350
|
+
* paged on directly live in it.
|
|
1351
|
+
*/
|
|
1352
|
+
expect(mountedTableModels).toContain(UserEmail);
|
|
1353
|
+
expect(mountedTableModels).toContain(UserSMS);
|
|
1354
|
+
expect(mountedTableModels).toContain(UserCall);
|
|
1355
|
+
expect(mountedTableModels).toContain(UserWhatsApp);
|
|
1356
|
+
expect(mountedTableModels).toContain(UserTelegram);
|
|
1357
|
+
|
|
1358
|
+
await openTab("Workspace Apps");
|
|
1359
|
+
|
|
1360
|
+
expect(mountedTableModels).toContain(UserSlack);
|
|
1361
|
+
expect(mountedTableModels).toContain(UserMicrosoftTeams);
|
|
1362
|
+
|
|
1363
|
+
await openTab("Push Notifications");
|
|
1364
|
+
|
|
1365
|
+
expect(mountedTableModels).toContain(UserPush);
|
|
1366
|
+
|
|
1367
|
+
await openTab("Webhooks");
|
|
1368
|
+
|
|
1369
|
+
expect(mountedTableModels).toContain(UserWebhook);
|
|
1370
|
+
|
|
1371
|
+
/*
|
|
1372
|
+
* Asserted as a set at the end as well, because the four channels an
|
|
1373
|
+
* administrator cannot add are exactly the ones parked on the later tabs —
|
|
1374
|
+
* a tab that stopped rendering would take the only surface those channels
|
|
1375
|
+
* have with it.
|
|
1376
|
+
*/
|
|
1377
|
+
for (const model of NOTIFICATION_METHOD_MODELS) {
|
|
1378
|
+
expect(mountedTableModels).toContain(model);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
test("draws none of the administrative surface", async () => {
|
|
1383
|
+
const container: HTMLElement = await renderSelfPage();
|
|
1384
|
+
|
|
1385
|
+
expect(
|
|
1386
|
+
screen.queryByTestId("admin-notification-method-list"),
|
|
1387
|
+
).not.toBeInTheDocument();
|
|
1388
|
+
expect(
|
|
1389
|
+
screen.queryByTestId("no-methods-empty-state"),
|
|
1390
|
+
).not.toBeInTheDocument();
|
|
1391
|
+
expect(screen.queryByText("Add notification method")).toBeNull();
|
|
1392
|
+
|
|
1393
|
+
/*
|
|
1394
|
+
* And none of its copy. Masked identifiers and "waiting for them to verify"
|
|
1395
|
+
* are both written for somebody who is not the owner; addressed to the
|
|
1396
|
+
* owner they describe a restriction that is not real, on a page where they
|
|
1397
|
+
* can do the verifying themselves.
|
|
1398
|
+
*/
|
|
1399
|
+
expect(container.textContent).not.toContain(MASKED_EMAIL);
|
|
1400
|
+
expect(container.textContent).not.toContain(
|
|
1401
|
+
`Waiting for ${TARGET_USER_FIRST_NAME} to verify`,
|
|
1402
|
+
);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
test("asks the admin capability for nothing at all", async () => {
|
|
1406
|
+
await renderSelfPage();
|
|
1407
|
+
|
|
1408
|
+
/*
|
|
1409
|
+
* Not only the list read. The endpoint also carries the add, the removal,
|
|
1410
|
+
* the impact preview and the resend, and none of them is this page's to
|
|
1411
|
+
* call for its own owner — the ordinary settings components do all of it
|
|
1412
|
+
* against the models directly.
|
|
1413
|
+
*/
|
|
1414
|
+
const adminCalls: Array<unknown> = [
|
|
1415
|
+
...apiGetMock.mock.calls,
|
|
1416
|
+
...apiPostMock.mock.calls,
|
|
1417
|
+
...apiDeleteMock.mock.calls,
|
|
1418
|
+
].filter((call: any): boolean => {
|
|
1419
|
+
return String(call[0].url).includes("/user-notification-method-admin/");
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
expect(adminCalls).toHaveLength(0);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
test("says these are the same settings they already have", async () => {
|
|
1426
|
+
const container: HTMLElement = await renderSelfPage();
|
|
1427
|
+
|
|
1428
|
+
/*
|
|
1429
|
+
* The banner the section renders for an owner. Somebody who arrives here
|
|
1430
|
+
* from a colleague's row has to be able to tell at a glance that the page
|
|
1431
|
+
* has changed hands — and the answer to "why does this look different from
|
|
1432
|
+
* User Settings" is that it is not different.
|
|
1433
|
+
*/
|
|
1434
|
+
expect(container.textContent).toContain(
|
|
1435
|
+
"This is your own on-call configuration",
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
/*
|
|
1441
|
+
* What the page does with a row it cannot make sense of.
|
|
1442
|
+
*
|
|
1443
|
+
* Every one of these is a shape the server does not send today. They are here
|
|
1444
|
+
* because parseMethod exists — a page that parses field by field has decided
|
|
1445
|
+
* that a surprising payload is a thing that happens, and the value of that
|
|
1446
|
+
* decision is entirely in what it does next.
|
|
1447
|
+
*/
|
|
1448
|
+
describe("a row the server sends malformed", () => {
|
|
1449
|
+
test("a row with no id or no channel is dropped, not drawn blank", async () => {
|
|
1450
|
+
respondWithMethods([
|
|
1451
|
+
VERIFIED_EMAIL,
|
|
1452
|
+
// No id: there is nothing to remove or resend against.
|
|
1453
|
+
{ methodType: "SMS", maskedIdentifier: MASKED_PHONE } as JSONObject,
|
|
1454
|
+
// No channel: every url this page builds puts the channel in the path.
|
|
1455
|
+
{ methodId: SMS_METHOD_ID, maskedIdentifier: MASKED_PHONE } as JSONObject,
|
|
1456
|
+
// Neither.
|
|
1457
|
+
{ maskedIdentifier: MASKED_WEBHOOK } as JSONObject,
|
|
1458
|
+
]);
|
|
1459
|
+
|
|
1460
|
+
await renderPage();
|
|
1461
|
+
|
|
1462
|
+
expect(methodRows()).toHaveLength(1);
|
|
1463
|
+
expect(rowFor(MASKED_EMAIL).textContent).toContain("Email");
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
test("a channel with no masked identifier is blank, never the word undefined", async () => {
|
|
1467
|
+
respondWithMethods([
|
|
1468
|
+
{
|
|
1469
|
+
methodId: EMAIL_METHOD_ID,
|
|
1470
|
+
methodType: "Email",
|
|
1471
|
+
isVerified: true,
|
|
1472
|
+
isAdminAddable: true,
|
|
1473
|
+
} as JSONObject,
|
|
1474
|
+
]);
|
|
1475
|
+
|
|
1476
|
+
const container: HTMLElement = await renderPage();
|
|
1477
|
+
|
|
1478
|
+
const rows: Array<HTMLElement> = methodRows();
|
|
1479
|
+
|
|
1480
|
+
expect(rows).toHaveLength(1);
|
|
1481
|
+
expect(rows[0]!.textContent).toContain("Email");
|
|
1482
|
+
|
|
1483
|
+
/*
|
|
1484
|
+
* The row is still worth drawing — the channel and the controls are the
|
|
1485
|
+
* useful part — but "undefined" beside a channel name reads as an
|
|
1486
|
+
* identifier, and it is the one word on this page nobody can act on.
|
|
1487
|
+
*/
|
|
1488
|
+
expect(container.textContent).not.toContain("undefined");
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
test("an absent verification flag reads as unverified", async () => {
|
|
1492
|
+
respondWithMethods([
|
|
1493
|
+
{
|
|
1494
|
+
methodId: SMS_METHOD_ID,
|
|
1495
|
+
methodType: "SMS",
|
|
1496
|
+
maskedIdentifier: MASKED_PHONE,
|
|
1497
|
+
} as JSONObject,
|
|
1498
|
+
]);
|
|
1499
|
+
|
|
1500
|
+
await renderPage();
|
|
1501
|
+
|
|
1502
|
+
/*
|
|
1503
|
+
* The safe direction, and not an arbitrary one: an unverified row is drawn
|
|
1504
|
+
* as still waiting on its owner, which is a true statement about a method
|
|
1505
|
+
* whose state is unknown. The other default would print "Verified" over a
|
|
1506
|
+
* channel nothing has confirmed.
|
|
1507
|
+
*/
|
|
1508
|
+
expect(rowFor(MASKED_PHONE).textContent).toContain(
|
|
1509
|
+
`Waiting for ${TARGET_USER_FIRST_NAME} to verify`,
|
|
1510
|
+
);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
test("an unverified channel an admin could not have added offers no resend", async () => {
|
|
1514
|
+
const UNVERIFIED_TELEGRAM: JSONObject = methodJson({
|
|
1515
|
+
methodId: WEBHOOK_METHOD_ID,
|
|
1516
|
+
methodType: "Telegram",
|
|
1517
|
+
maskedIdentifier: "@ja•••",
|
|
1518
|
+
isVerified: false,
|
|
1519
|
+
isAdminAddable: false,
|
|
1520
|
+
leakedRawValue: RAW_PHONE,
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
respondWithMethods([UNVERIFIED_TELEGRAM]);
|
|
1524
|
+
|
|
1525
|
+
await renderPage();
|
|
1526
|
+
|
|
1527
|
+
const row: HTMLElement = rowFor("@ja•••");
|
|
1528
|
+
|
|
1529
|
+
/*
|
|
1530
|
+
* Two separate conditions, and this row is the one that tells them apart:
|
|
1531
|
+
* unverified is not sufficient. Telegram's code is not something this page
|
|
1532
|
+
* can cause to be sent — the account holder has to message the bot — so a
|
|
1533
|
+
* "Resend code" button here is a button that can only fail.
|
|
1534
|
+
*/
|
|
1535
|
+
expect(within(row).queryByText("Resend code")).toBeNull();
|
|
1536
|
+
expect(within(row).getByText("Remove")).toBeInTheDocument();
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
/*
|
|
1541
|
+
* The numbers in the removal confirmation, which are the last thing an
|
|
1542
|
+
* administrator reads before a cascade.
|
|
1543
|
+
*/
|
|
1544
|
+
describe("the deletion preview's arithmetic", () => {
|
|
1545
|
+
test("counts of one are not pluralised", async () => {
|
|
1546
|
+
await renderPage();
|
|
1547
|
+
|
|
1548
|
+
respondWithDeletionImpact({
|
|
1549
|
+
rulesDeletedCount: 1,
|
|
1550
|
+
coverageLostCount: 1,
|
|
1551
|
+
verifiedMethodCountAfterDeletion: 1,
|
|
1552
|
+
reachability: "PartiallyReady",
|
|
1553
|
+
isFallbackEnabled: true,
|
|
1554
|
+
isTruncated: false,
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
1558
|
+
|
|
1559
|
+
const preview: HTMLElement = await screen.findByTestId("deletion-preview");
|
|
1560
|
+
|
|
1561
|
+
expect(preview.textContent).toContain(
|
|
1562
|
+
"1 notification rule will be deleted",
|
|
1563
|
+
);
|
|
1564
|
+
expect(preview.textContent).not.toContain("rules will be deleted");
|
|
1565
|
+
expect(preview.textContent).toContain("1 severity");
|
|
1566
|
+
expect(preview.textContent).not.toContain("severities");
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
test("a removal that costs no coverage does not invent a loss", async () => {
|
|
1570
|
+
await renderPage();
|
|
1571
|
+
|
|
1572
|
+
respondWithDeletionImpact({
|
|
1573
|
+
rulesDeletedCount: 2,
|
|
1574
|
+
coverageLostCount: 0,
|
|
1575
|
+
verifiedMethodCountAfterDeletion: 1,
|
|
1576
|
+
reachability: "Ready",
|
|
1577
|
+
isFallbackEnabled: true,
|
|
1578
|
+
isTruncated: false,
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
1582
|
+
|
|
1583
|
+
const preview: HTMLElement = await screen.findByTestId("deletion-preview");
|
|
1584
|
+
|
|
1585
|
+
/*
|
|
1586
|
+
* Two rules go, and every severity still has one. Saying "leaving 0
|
|
1587
|
+
* severities with no rule at all" would be true and would still read as a
|
|
1588
|
+
* warning, which is how a confirmation earns its dismissal.
|
|
1589
|
+
*/
|
|
1590
|
+
expect(preview.textContent).toContain(
|
|
1591
|
+
"2 notification rules will be deleted",
|
|
1592
|
+
);
|
|
1593
|
+
expect(preview.textContent).not.toContain("leaving");
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
test("somebody who keeps a verified method is not called unreachable", async () => {
|
|
1597
|
+
await renderPage();
|
|
1598
|
+
|
|
1599
|
+
respondWithDeletionImpact({
|
|
1600
|
+
rulesDeletedCount: 1,
|
|
1601
|
+
coverageLostCount: 0,
|
|
1602
|
+
verifiedMethodCountAfterDeletion: 2,
|
|
1603
|
+
reachability: "Ready",
|
|
1604
|
+
isFallbackEnabled: true,
|
|
1605
|
+
isTruncated: false,
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
1609
|
+
|
|
1610
|
+
await screen.findByTestId("deletion-preview");
|
|
1611
|
+
|
|
1612
|
+
/*
|
|
1613
|
+
* The one sentence in this modal that is worth interrupting for only works
|
|
1614
|
+
* while it is rare. Printed over a removal that leaves two working channels
|
|
1615
|
+
* behind, it is the sentence somebody learns to scroll past.
|
|
1616
|
+
*/
|
|
1617
|
+
expect(document.body.textContent).not.toContain(
|
|
1618
|
+
"will have no verified notification method left",
|
|
1619
|
+
);
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test("a truncated count admits it is a lower bound", async () => {
|
|
1623
|
+
await renderPage();
|
|
1624
|
+
|
|
1625
|
+
respondWithDeletionImpact({
|
|
1626
|
+
rulesDeletedCount: 500,
|
|
1627
|
+
coverageLostCount: 3,
|
|
1628
|
+
verifiedMethodCountAfterDeletion: 1,
|
|
1629
|
+
reachability: "PartiallyReady",
|
|
1630
|
+
isFallbackEnabled: true,
|
|
1631
|
+
isTruncated: true,
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
1635
|
+
|
|
1636
|
+
const preview: HTMLElement = await screen.findByTestId("deletion-preview");
|
|
1637
|
+
|
|
1638
|
+
/*
|
|
1639
|
+
* "500 rules will be deleted" is a number an administrator will weigh the
|
|
1640
|
+
* decision against, and a capped read that does not say it was capped hands
|
|
1641
|
+
* them a floor dressed as a total.
|
|
1642
|
+
*/
|
|
1643
|
+
expect(preview.textContent).toContain("lower bound");
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
test("a payload that is not an impact is not drawn as one", async () => {
|
|
1647
|
+
await renderPage();
|
|
1648
|
+
|
|
1649
|
+
// A 200 whose body has none of the counts in it.
|
|
1650
|
+
respondWithDeletionImpact({ methods: [] });
|
|
1651
|
+
|
|
1652
|
+
clickButton(rowFor(MASKED_EMAIL), "Remove");
|
|
1653
|
+
|
|
1654
|
+
await waitForSettled();
|
|
1655
|
+
|
|
1656
|
+
/*
|
|
1657
|
+
* Cast rather than parsed, this renders "undefined notification rules will
|
|
1658
|
+
* be deleted" — and, worse, silently answers the reachability question,
|
|
1659
|
+
* because `undefined === 0` is false and the sentence about being left with
|
|
1660
|
+
* no verified method simply does not appear. The confirmation falls back to
|
|
1661
|
+
* the general warning instead, which is true without any numbers at all.
|
|
1662
|
+
*/
|
|
1663
|
+
expect(screen.queryByTestId("deletion-preview")).not.toBeInTheDocument();
|
|
1664
|
+
expect(document.body.textContent).not.toContain("undefined");
|
|
1665
|
+
expect(document.body.textContent).toContain(
|
|
1666
|
+
"every notification rule that points at it goes with it",
|
|
1667
|
+
);
|
|
1668
|
+
|
|
1669
|
+
// And it is still a removal the admin can go through with.
|
|
1670
|
+
fireEvent.click(within(modal()).getByText("Remove"));
|
|
1671
|
+
|
|
1672
|
+
await waitFor((): void => {
|
|
1673
|
+
expect(apiDeleteMock).toHaveBeenCalled();
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
describe("a write the server refuses", () => {
|
|
1679
|
+
test("a refused removal keeps the row and says why", async () => {
|
|
1680
|
+
await renderPage();
|
|
1681
|
+
|
|
1682
|
+
apiDeleteMock.mockResolvedValue(
|
|
1683
|
+
new HTTPErrorResponse(
|
|
1684
|
+
400,
|
|
1685
|
+
{ message: "This method has already been removed" },
|
|
1686
|
+
{},
|
|
1687
|
+
) as never,
|
|
1688
|
+
);
|
|
1689
|
+
|
|
1690
|
+
clickButton(rowFor(MASKED_PHONE), "Remove");
|
|
1691
|
+
|
|
1692
|
+
fireEvent.click(within(modal()).getByText("Remove"));
|
|
1693
|
+
|
|
1694
|
+
await waitFor((): void => {
|
|
1695
|
+
expect(apiDeleteMock).toHaveBeenCalled();
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
const messages: Array<HTMLElement> =
|
|
1699
|
+
await screen.findAllByText(/already been removed/);
|
|
1700
|
+
|
|
1701
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
1702
|
+
|
|
1703
|
+
/*
|
|
1704
|
+
* And the list underneath is untouched. Removing the row optimistically and
|
|
1705
|
+
* then failing would leave an administrator looking at a list that says the
|
|
1706
|
+
* method is gone, over an account where it is still live.
|
|
1707
|
+
*/
|
|
1708
|
+
expect(methodRows()).toHaveLength(3);
|
|
1709
|
+
expect(rowFor(MASKED_PHONE)).toBeInTheDocument();
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
test("a successful removal re-reads the list and the readiness summary", async () => {
|
|
1713
|
+
await renderPage();
|
|
1714
|
+
|
|
1715
|
+
apiGetMock.mockClear();
|
|
1716
|
+
|
|
1717
|
+
clickButton(rowFor(MASKED_PHONE), "Remove");
|
|
1718
|
+
|
|
1719
|
+
fireEvent.click(within(modal()).getByText("Remove"));
|
|
1720
|
+
|
|
1721
|
+
await waitFor((): void => {
|
|
1722
|
+
expect(apiDeleteMock).toHaveBeenCalled();
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
await waitFor((): void => {
|
|
1726
|
+
const urls: Array<string> = apiGetMock.mock.calls.map(
|
|
1727
|
+
(call: Array<any>): string => {
|
|
1728
|
+
return String(call[0].url);
|
|
1729
|
+
},
|
|
1730
|
+
);
|
|
1731
|
+
|
|
1732
|
+
expect(
|
|
1733
|
+
urls.some((url: string): boolean => {
|
|
1734
|
+
return (
|
|
1735
|
+
url.includes("/user-notification-method-admin/user/") &&
|
|
1736
|
+
!url.includes("/deletion-impact")
|
|
1737
|
+
);
|
|
1738
|
+
}),
|
|
1739
|
+
).toBe(true);
|
|
1740
|
+
|
|
1741
|
+
/*
|
|
1742
|
+
* And readiness with `refresh`, for the same reason the add does it: a
|
|
1743
|
+
* removal is the change most likely to have made somebody unreachable,
|
|
1744
|
+
* and a summary answered out of the service's 60s cache would show the
|
|
1745
|
+
* state from before it.
|
|
1746
|
+
*/
|
|
1747
|
+
expect(
|
|
1748
|
+
urls.some((url: string): boolean => {
|
|
1749
|
+
return url.includes("/on-call-readiness/") && url.includes("refresh");
|
|
1750
|
+
}),
|
|
1751
|
+
).toBe(true);
|
|
1752
|
+
});
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
test("a refused resend does not claim the code was sent", async () => {
|
|
1756
|
+
await renderPage();
|
|
1757
|
+
|
|
1758
|
+
apiPostMock.mockResolvedValue(
|
|
1759
|
+
new HTTPErrorResponse(
|
|
1760
|
+
500,
|
|
1761
|
+
{ message: "the SMS provider is down" },
|
|
1762
|
+
{},
|
|
1763
|
+
) as never,
|
|
1764
|
+
);
|
|
1765
|
+
|
|
1766
|
+
clickButton(rowFor(MASKED_PHONE), "Resend code");
|
|
1767
|
+
|
|
1768
|
+
fireEvent.click(within(modal()).getByText("Resend code"));
|
|
1769
|
+
|
|
1770
|
+
await waitFor((): void => {
|
|
1771
|
+
expect(apiPostMock).toHaveBeenCalled();
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
await waitForSettled();
|
|
1775
|
+
|
|
1776
|
+
/*
|
|
1777
|
+
* "Code sent" over a code that was not sent is the worst of the three
|
|
1778
|
+
* outcomes here: the admin stops, tells the responder to go and look, and
|
|
1779
|
+
* the responder finds nothing.
|
|
1780
|
+
*/
|
|
1781
|
+
expect(document.body.textContent).toContain("the SMS provider is down");
|
|
1782
|
+
expect(document.body.textContent).not.toContain("Code sent");
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
test("a sent code says whose job the rest of it is", async () => {
|
|
1786
|
+
await renderPage();
|
|
1787
|
+
|
|
1788
|
+
clickButton(rowFor(MASKED_PHONE), "Resend code");
|
|
1789
|
+
|
|
1790
|
+
fireEvent.click(within(modal()).getByText("Resend code"));
|
|
1791
|
+
|
|
1792
|
+
await waitFor((): void => {
|
|
1793
|
+
expect(apiPostMock).toHaveBeenCalled();
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
await waitForSettled();
|
|
1797
|
+
|
|
1798
|
+
expect(document.body.textContent).toContain("Code sent");
|
|
1799
|
+
expect(document.body.textContent).toContain(
|
|
1800
|
+
"needs to enter it in their own user settings",
|
|
1801
|
+
);
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
test("the resend confirmation names the mask, not the number", async () => {
|
|
1805
|
+
await renderPage();
|
|
1806
|
+
|
|
1807
|
+
clickButton(rowFor(MASKED_PHONE), "Resend code");
|
|
1808
|
+
|
|
1809
|
+
expect(document.body.textContent).toContain(MASKED_PHONE);
|
|
1810
|
+
|
|
1811
|
+
for (const raw of ALL_RAW_IDENTIFIERS) {
|
|
1812
|
+
expect(document.body.innerHTML).not.toContain(raw);
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
test("closing the add form writes nothing", async () => {
|
|
1817
|
+
await renderPage();
|
|
1818
|
+
|
|
1819
|
+
clickButton(document.body, "Add notification method");
|
|
1820
|
+
|
|
1821
|
+
fireEvent.change(
|
|
1822
|
+
screen.getByPlaceholderText("you@company.com or +15551234567"),
|
|
1823
|
+
{ target: { value: RAW_PHONE } },
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
fireEvent.click(within(modal()).getByText("Cancel"));
|
|
1827
|
+
|
|
1828
|
+
await waitForSettled();
|
|
1829
|
+
|
|
1830
|
+
expect(apiPostMock).not.toHaveBeenCalled();
|
|
1831
|
+
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
test("the form opens on the one channel no project setting can disable", async () => {
|
|
1835
|
+
await renderPage();
|
|
1836
|
+
|
|
1837
|
+
clickButton(document.body, "Add notification method");
|
|
1838
|
+
|
|
1839
|
+
fireEvent.change(
|
|
1840
|
+
screen.getByPlaceholderText("you@company.com or +15551234567"),
|
|
1841
|
+
{ target: { value: RAW_EMAIL } },
|
|
1842
|
+
);
|
|
1843
|
+
|
|
1844
|
+
fireEvent.click(within(modal()).getByText("Add"));
|
|
1845
|
+
|
|
1846
|
+
await waitFor((): void => {
|
|
1847
|
+
expect(apiPostMock).toHaveBeenCalled();
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
/*
|
|
1851
|
+
* Submitted without touching the dropdown, which is the whole point of
|
|
1852
|
+
* preselecting it: SMS, Call and WhatsApp each have a per-project switch
|
|
1853
|
+
* behind them, and a default that lands on a disabled channel turns the
|
|
1854
|
+
* commonest action on this page into a refusal.
|
|
1855
|
+
*/
|
|
1856
|
+
expect(apiPostMock.mock.calls[0]![0].data.methodType).toBe("Email");
|
|
1857
|
+
});
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
describe("who gets the controls", () => {
|
|
1861
|
+
test("a master admin keeps them with no project permission at all", async () => {
|
|
1862
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(true);
|
|
1863
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
1864
|
+
|
|
1865
|
+
await renderPage();
|
|
1866
|
+
|
|
1867
|
+
/*
|
|
1868
|
+
* A master admin holds no project permissions by construction — they are
|
|
1869
|
+
* not a member of the project — so a page checking only the project triple
|
|
1870
|
+
* hides this section from the one account that exists to repair things
|
|
1871
|
+
* nobody else can.
|
|
1872
|
+
*/
|
|
1873
|
+
expect(screen.getByText("Add notification method")).toBeInTheDocument();
|
|
1874
|
+
expect(
|
|
1875
|
+
within(rowFor(MASKED_EMAIL)).getByText("Remove"),
|
|
1876
|
+
).toBeInTheDocument();
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
test("a reader looking at an empty account is told how to get the permission", async () => {
|
|
1880
|
+
jest
|
|
1881
|
+
.spyOn(PermissionUtil, "getAllPermissions")
|
|
1882
|
+
.mockReturnValue([Permission.ReadProjectUserNotificationRule]);
|
|
1883
|
+
|
|
1884
|
+
respondWithMethods([]);
|
|
1885
|
+
|
|
1886
|
+
const container: HTMLElement = await renderPage();
|
|
1887
|
+
|
|
1888
|
+
expect(screen.getByTestId("no-methods-empty-state")).toBeInTheDocument();
|
|
1889
|
+
|
|
1890
|
+
/*
|
|
1891
|
+
* The same alarming fact, with the action that is actually open to THIS
|
|
1892
|
+
* reader. Offering them "Add one for Jane" would point at a button they
|
|
1893
|
+
* cannot see, on a call the API would refuse.
|
|
1894
|
+
*/
|
|
1895
|
+
expect(container.textContent).toContain("Manage User Notification Methods");
|
|
1896
|
+
expect(container.textContent).not.toContain(
|
|
1897
|
+
`Add one for ${TARGET_USER_FIRST_NAME}`,
|
|
1898
|
+
);
|
|
1899
|
+
});
|
|
1900
|
+
});
|