@oneuptime/common 12.0.7 → 12.0.9
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/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +1 -1
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +1 -1
- package/Models/DatabaseModels/AIConversation.ts +32 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +41 -0
- package/Models/DatabaseModels/AlertEpisodeMember.ts +27 -0
- package/Models/DatabaseModels/IncidentEpisodeMember.ts +28 -0
- package/Models/DatabaseModels/Index.ts +12 -4
- package/Models/DatabaseModels/{TelemetryEntity.ts → InventoryItem.ts} +184 -9
- package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
- package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +33 -7
- package/Models/DatabaseModels/NetworkDevice.ts +141 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +699 -0
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +467 -0
- package/Models/DatabaseModels/NetworkTopologySuppression.ts +429 -0
- package/Models/DatabaseModels/OnCallDutyPolicyFeed.ts +9 -0
- package/Models/DatabaseModels/Project.ts +78 -0
- package/Models/DatabaseModels/UserCall.ts +42 -0
- package/Models/DatabaseModels/UserEmail.ts +44 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +425 -55
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +24 -2
- package/Models/DatabaseModels/UserPush.ts +49 -0
- package/Models/DatabaseModels/UserSMS.ts +43 -0
- package/Models/DatabaseModels/UserTelegram.ts +59 -0
- package/Models/DatabaseModels/UserWebhook.ts +52 -0
- package/Models/DatabaseModels/UserWhatsApp.ts +41 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
- package/Server/API/AIChatAPI.ts +315 -1
- package/Server/API/DashboardAPI.ts +217 -1
- package/Server/API/OnCallReadinessAPI.ts +841 -0
- package/Server/API/TeamComplianceAPI.ts +69 -17
- package/Server/API/TelemetryAPI.ts +220 -12
- package/Server/API/UserAPI.ts +16 -1
- package/Server/EnvironmentConfig.ts +52 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +24 -0
- package/Server/Infrastructure/Queue.ts +78 -13
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
- package/Server/Services/AIService.ts +7 -0
- package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
- package/Server/Services/AlertSeverityService.ts +63 -0
- package/Server/Services/DashboardService.ts +9 -10
- package/Server/Services/DatabaseService.ts +32 -2
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
- package/Server/Services/IncidentSeverityService.ts +76 -0
- package/Server/Services/Index.ts +12 -4
- package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
- package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +7 -4
- package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +203 -21
- package/Server/Services/LogAggregationService.ts +45 -8
- package/Server/Services/MetricAggregationService.ts +121 -0
- package/Server/Services/MetricService.ts +7 -7
- package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
- package/Server/Services/NetworkDeviceLinkService.ts +84 -0
- package/Server/Services/NetworkDeviceService.ts +140 -0
- package/Server/Services/NetworkSiteService.ts +77 -25
- package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
- package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
- package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
- package/Server/Services/OnCallReadinessService.ts +2803 -0
- package/Server/Services/OnCallSetupReminderService.ts +955 -0
- package/Server/Services/ProfileAggregationService.ts +123 -0
- package/Server/Services/StatusPageService.ts +9 -10
- package/Server/Services/TeamComplianceService.ts +429 -252
- package/Server/Services/UserCallService.ts +26 -1
- package/Server/Services/UserEmailService.ts +26 -1
- package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
- package/Server/Services/UserNotificationRuleService.ts +3812 -333
- package/Server/Services/UserOnCallLogService.ts +561 -48
- package/Server/Services/UserPushService.ts +29 -0
- package/Server/Services/UserService.ts +11 -0
- package/Server/Services/UserSmsService.ts +26 -1
- package/Server/Services/UserTelegramService.ts +24 -1
- package/Server/Services/UserWebhookService.ts +28 -1
- package/Server/Services/UserWhatsAppService.ts +24 -1
- package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
- package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
- package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
- package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
- package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
- package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
- package/Server/Types/Workflow/Components/API/Get.ts +1 -1
- package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
- package/Server/Types/Workflow/Components/API/Post.ts +1 -1
- package/Server/Types/Workflow/Components/API/Put.ts +1 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
- package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
- package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
- package/Server/Types/Workflow/Components/Email.ts +25 -7
- package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
- package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
- package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
- package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
- package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
- package/Server/Utils/AI/Toolbox/Index.ts +48 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
- package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
- package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
- package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
- package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
- package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
- package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
- package/Server/Utils/ClientIp.ts +221 -0
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
- package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
- package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
- package/Server/Utils/Express.ts +12 -17
- package/Server/Utils/LLM/LLMService.ts +85 -8
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
- package/Server/Utils/SSRFProtection.ts +98 -23
- package/Server/Utils/StartServer.ts +12 -3
- package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
- package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
- package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
- package/Tests/Models/InventoryItemModel.test.ts +174 -0
- package/Tests/Models/InventoryItemNaming.test.ts +302 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
- package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
- package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
- package/Tests/Server/API/Helpers.ts +24 -15
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
- package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
- package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
- package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
- package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
- package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +248 -0
- package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
- package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
- package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
- package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
- package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
- package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
- package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
- package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
- package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
- package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
- package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
- package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
- package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
- package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
- package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
- package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
- package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
- package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
- package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
- package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
- package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
- package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
- package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
- package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
- package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
- package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/ClientIp.test.ts +438 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
- package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
- package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
- package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
- package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/IP/IP.test.ts +263 -0
- package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
- package/Tests/Types/IP/IPv6.test.ts +12 -1
- package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
- package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
- package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +491 -0
- package/Tests/Types/Workflow/Templates.test.ts +1218 -0
- package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
- package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
- package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
- package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
- package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +893 -0
- package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +522 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +193 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +388 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +210 -0
- package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
- package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
- package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
- package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
- package/Tests/UI/EsbuildConfig.test.ts +607 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
- package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
- package/Tests/UI/Utils/Platform.test.ts +147 -0
- package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +727 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Tests/__mocks__/bullmq.js +55 -0
- package/Types/AI/AIChatMessageStatus.ts +8 -1
- package/Types/AI/AIChatTypes.ts +12 -0
- package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
- package/Types/Exception/ExceptionCode.ts +2 -0
- package/Types/Exception/ServiceUnavailableException.ts +8 -0
- package/Types/Exception/TooManyRequestsException.ts +8 -0
- package/Types/IP/IP.ts +93 -47
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +80 -3
- package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
- package/Types/OnCallDutyPolicy/Layer.ts +203 -149
- package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
- package/Types/Permission.ts +193 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntityRelationshipType.ts +1 -1
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +28 -1
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +77 -27
- package/Types/Workflow/Components/Discord.ts +1 -0
- package/Types/Workflow/Components/Email.ts +12 -3
- package/Types/Workflow/Components/JavaScript.ts +7 -0
- package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
- package/Types/Workflow/Components/Slack.ts +1 -0
- package/Types/Workflow/Components/Telegram.ts +1 -0
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +543 -0
- package/Types/Workflow/Templates.ts +2368 -0
- package/UI/Components/Calendar/Calendar.css +43 -0
- package/UI/Components/Calendar/Calendar.tsx +8 -0
- package/UI/Components/Card/Card.tsx +2 -2
- package/UI/Components/Checkbox/Checkbox.tsx +16 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +71 -15
- package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
- package/UI/Components/Input/Input.tsx +32 -3
- package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
- package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
- package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
- package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
- package/UI/Components/LogsViewer/types.ts +23 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
- package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
- package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
- package/UI/Components/Table/Table.tsx +49 -16
- package/UI/Components/Table/TableBody.tsx +53 -28
- package/UI/Components/Table/TableHeader.tsx +13 -0
- package/UI/Components/Table/TableRow.tsx +58 -26
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +341 -8
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +79 -9
- package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
- package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
- package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
- package/UI/Components/Workflow/GraphLint.ts +628 -0
- package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +528 -0
- package/UI/Components/Workflow/ModelSchema.ts +278 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/UseRunWatch.ts +212 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/VariableModal.tsx +6 -2
- package/UI/Components/Workflow/Workflow.tsx +126 -7
- package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
- package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
- package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
- package/UI/Utils/Platform.ts +149 -0
- package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
- package/UI/esbuild-config.js +22 -1
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +917 -155
- package/Utils/Telemetry/CrossSignalScope.ts +502 -0
- package/Utils/Telemetry/EntityKey.ts +71 -5
- package/Utils/Telemetry/EntityRelationship.ts +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
- package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +12 -4
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +212 -29
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
- package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +52 -25
- package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
- package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +80 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
- package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
- package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +237 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +165 -13
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
- package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
- package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
- package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +129 -18
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +45 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +72 -13
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
- package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +1 -0
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +54 -0
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/DashboardService.js +10 -9
- package/build/dist/Server/Services/DashboardService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +24 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +12 -4
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
- package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +9 -6
- package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +158 -23
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
- package/build/dist/Server/Services/LogAggregationService.js +27 -8
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +80 -0
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +6 -6
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
- package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +53 -13
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
- package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
- package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
- package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
- package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +11 -10
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceService.js +312 -160
- package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +24 -1
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +24 -1
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +26 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +10 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +24 -1
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserTelegramService.js +22 -1
- package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
- package/build/dist/Server/Services/UserWebhookService.js +25 -1
- package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
- package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
- package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
- package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
- package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
- package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
- package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
- package/build/dist/Server/Utils/ClientIp.js +137 -0
- package/build/dist/Server/Utils/ClientIp.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
- package/build/dist/Server/Utils/Express.js +12 -12
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +82 -21
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
- package/build/dist/Types/AI/AIChatTypes.js +12 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +2 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
- package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
- package/build/dist/Types/IP/IP.js +87 -43
- package/build/dist/Types/IP/IP.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/Permission.js +174 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +28 -1
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +68 -26
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Discord.js +1 -0
- package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Email.js +12 -3
- package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
- package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
- package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Slack.js +1 -0
- package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
- package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +338 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +2111 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +47 -17
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +13 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
- package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
- package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
- package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +27 -15
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +24 -18
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +9 -1
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +29 -21
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +261 -14
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +35 -7
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
- package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +425 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
- package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +320 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +156 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
- package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
- package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +92 -7
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
- package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
- package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
- package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
- package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
- package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Platform.js +118 -0
- package/build/dist/UI/Utils/Platform.js.map +1 -0
- package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
- package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +661 -126
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +57 -5
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
- package/jest.config.json +1 -0
- package/package.json +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +0 -1
|
@@ -0,0 +1,2146 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
cleanup,
|
|
12
|
+
render,
|
|
13
|
+
screen,
|
|
14
|
+
waitFor,
|
|
15
|
+
within,
|
|
16
|
+
} from "@testing-library/react";
|
|
17
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
18
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Users > View > Notification Rules, and the table component it now shares with
|
|
22
|
+
* the four self-serve settings pages.
|
|
23
|
+
*
|
|
24
|
+
* Phase 3 is the first time one person's paging configuration is editable by
|
|
25
|
+
* another person, so the failures worth writing down here are not "the page
|
|
26
|
+
* crashed" - they are the quiet ones:
|
|
27
|
+
*
|
|
28
|
+
* - THE WRONG SEVERITY AXIS. A rule is tied to its band by
|
|
29
|
+
* `incidentSeverityId` or `alertSeverityId`, and the severity MODEL does not
|
|
30
|
+
* follow from the rule type the way the names suggest: an alert episode is
|
|
31
|
+
* banded by AlertSeverity, an incident episode by IncidentSeverity. Get
|
|
32
|
+
* either axis wrong and nothing throws. The table simply lists rules for
|
|
33
|
+
* EVERY severity, so a Sev 4 pages exactly like a Sev 1, and rules created
|
|
34
|
+
* from it are stamped with a column nobody filters on. The two severity
|
|
35
|
+
* models in this file therefore return DISJOINT id sets, which is the only
|
|
36
|
+
* fixture shape in which "right column, wrong model" is visible at all.
|
|
37
|
+
*
|
|
38
|
+
* - A SELF-SERVE REGRESSION. Four ~370-line copies became one component with
|
|
39
|
+
* props. Everything a copy-paste page got wrong loudly, a props object gets
|
|
40
|
+
* wrong silently, and these four pages are the ones every existing user
|
|
41
|
+
* already relies on, so their query, their created row, their stored
|
|
42
|
+
* preferences key and their card copy are pinned against the pre-extraction
|
|
43
|
+
* source.
|
|
44
|
+
*
|
|
45
|
+
* - AN ADMIN TYPING IN SOMEBODY ELSE'S PHONE NUMBER. Rules are fully editable
|
|
46
|
+
* on this page; notification METHODS are read-only and masked, deliberately.
|
|
47
|
+
* Letting an admin add a device or address to another account would point
|
|
48
|
+
* that person's pages at hardware the admin controls, and would not even
|
|
49
|
+
* solve the case it appears to solve - a responder with no methods needs a
|
|
50
|
+
* device THEY hold and can verify. The admin's lever is a reminder, not a
|
|
51
|
+
* keyboard, and the assertions below are structural (no form controls, no
|
|
52
|
+
* table over a method model) rather than copy-deep, because the next person
|
|
53
|
+
* to be tempted will add a button, not a sentence.
|
|
54
|
+
*
|
|
55
|
+
* - AN ADMIN READING SOMEBODY ELSE'S PHONE NUMBER, which is the same defect
|
|
56
|
+
* one step earlier and is what this page originally did. The seven method
|
|
57
|
+
* models are scoped to the person who owns the device - the columns behind
|
|
58
|
+
* them are the raw number, the webhook bearer url, the push device token,
|
|
59
|
+
* the telegram chat id and the verification code - and that scope was
|
|
60
|
+
* briefly widened so this page could build its rule form's method dropdown.
|
|
61
|
+
* It is back, and this page now reads NO method model at all: everything it
|
|
62
|
+
* knows about methods, the card and the dropdown alike, comes from the
|
|
63
|
+
* readiness payload, masked server-side. The assertion is about the request,
|
|
64
|
+
* not the render, because a component that asks is already wrong even if it
|
|
65
|
+
* is refused.
|
|
66
|
+
*
|
|
67
|
+
* - A LEAKED IDENTIFIER. Every method fixture carries its raw value alongside
|
|
68
|
+
* the masked one under three plausible field names, so a component that
|
|
69
|
+
* starts reading `identifier`, or a parse that starts copying unknown keys
|
|
70
|
+
* through, fails here. This is a second barrier on purpose: the server-side
|
|
71
|
+
* masking tests prove the service masks, these prove the browser never
|
|
72
|
+
* un-masks.
|
|
73
|
+
*
|
|
74
|
+
* The page is never the gate. POST and PATCH on /api/user-notification-rule stay
|
|
75
|
+
* reachable with any member session, so nothing here is a security boundary -
|
|
76
|
+
* it is the surface declining to draw controls the server would refuse, and the
|
|
77
|
+
* assertions are written in those terms.
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
const PROJECT_ID_STRING: string = "10000000-0000-4000-8000-000000000001";
|
|
81
|
+
const SIGNED_IN_USER_ID_STRING: string = "20000000-0000-4000-8000-000000000002";
|
|
82
|
+
const TARGET_USER_ID_STRING: string = "30000000-0000-4000-8000-000000000003";
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* Two disjoint sets. Incident severities and alert severities are two different
|
|
86
|
+
* tables, and a fixture that reused an id between them could not tell a table
|
|
87
|
+
* that enumerated the wrong model from one that enumerated the right one.
|
|
88
|
+
*/
|
|
89
|
+
const INCIDENT_SEVERITY_ONE_ID: string = "41111111-1111-4111-8111-111111111111";
|
|
90
|
+
const INCIDENT_SEVERITY_TWO_ID: string = "42222222-2222-4222-8222-222222222222";
|
|
91
|
+
const ALERT_SEVERITY_ONE_ID: string = "51111111-1111-4111-8111-111111111111";
|
|
92
|
+
const ALERT_SEVERITY_TWO_ID: string = "52222222-2222-4222-8222-222222222222";
|
|
93
|
+
|
|
94
|
+
const INCIDENT_SEVERITY_ONE_NAME: string = "Sev One";
|
|
95
|
+
const INCIDENT_SEVERITY_TWO_NAME: string = "Sev Two";
|
|
96
|
+
const ALERT_SEVERITY_ONE_NAME: string = "Alert One";
|
|
97
|
+
const ALERT_SEVERITY_TWO_NAME: string = "Alert Two";
|
|
98
|
+
|
|
99
|
+
const TARGET_USER_NAME: string = "Jane Ops";
|
|
100
|
+
const TARGET_USER_FIRST_NAME: string = "Jane";
|
|
101
|
+
const TARGET_LOGIN_EMAIL: string = "jane.ops@example.com";
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* The values that must never survive the trip to the DOM. They are planted
|
|
105
|
+
* INSIDE the readiness payload, on the same objects that carry the masked ones,
|
|
106
|
+
* so none of this is a tautology.
|
|
107
|
+
*/
|
|
108
|
+
const RAW_EMAIL: string = "jane.ops.personal@example.com";
|
|
109
|
+
const RAW_PHONE: string = "+15551234821";
|
|
110
|
+
const RAW_TELEGRAM: string = "@janeops_oncall";
|
|
111
|
+
const RAW_WEBHOOK_URL: string = "https://hooks.example.com/T0P-53CR3T-T0K3N";
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* The id of each method ROW - UserEmail._id, UserSMS._id - which is exactly what
|
|
115
|
+
* UserNotificationRule.userEmailId / userSmsId reference. It is the one field on
|
|
116
|
+
* the readiness payload that is not for display: it is what lets this page point
|
|
117
|
+
* a rule AT a method without reading that method's row.
|
|
118
|
+
*
|
|
119
|
+
* A foreign key is not a secret. It is stored in plain sight on every rule its
|
|
120
|
+
* owner already has, and it addresses nothing on its own - nobody is paged by a
|
|
121
|
+
* uuid. Carrying it beside the mask is the whole of how the dropdown works.
|
|
122
|
+
*/
|
|
123
|
+
const EMAIL_METHOD_ID: string = "60000000-0000-4000-8000-000000000001";
|
|
124
|
+
const SMS_METHOD_ID: string = "60000000-0000-4000-8000-000000000002";
|
|
125
|
+
const TELEGRAM_METHOD_ID: string = "60000000-0000-4000-8000-000000000003";
|
|
126
|
+
|
|
127
|
+
// Exactly the shapes OnCallReadinessService.maskIdentifier emits.
|
|
128
|
+
const MASKED_EMAIL: string = "j•••@example.com";
|
|
129
|
+
const MASKED_PHONE: string = "+1 ••• ••• 4821";
|
|
130
|
+
const MASKED_TELEGRAM: string = "@ja•••";
|
|
131
|
+
|
|
132
|
+
const ALL_RAW_IDENTIFIERS: Array<string> = [
|
|
133
|
+
RAW_EMAIL,
|
|
134
|
+
RAW_PHONE,
|
|
135
|
+
RAW_TELEGRAM,
|
|
136
|
+
RAW_WEBHOOK_URL,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* Anything shaped like a whole address or a whole phone number. The masked forms
|
|
141
|
+
* deliberately match neither: a bullet is not a local-part character, and four
|
|
142
|
+
* trailing digits are too few to be a number. Un-mask any one identifier and
|
|
143
|
+
* both patterns start hitting.
|
|
144
|
+
*/
|
|
145
|
+
const UNMASKED_EMAIL_PATTERN: RegExp =
|
|
146
|
+
/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
|
|
147
|
+
const UNMASKED_PHONE_PATTERN: RegExp = /\+?\d[\d\s().-]{6,}\d/;
|
|
148
|
+
|
|
149
|
+
type ReadErrorMessageFunction = (error: unknown) => string;
|
|
150
|
+
|
|
151
|
+
const readErrorMessage: ReadErrorMessageFunction = (error: unknown): string => {
|
|
152
|
+
const message: unknown = (error as { message?: unknown } | null)?.message;
|
|
153
|
+
|
|
154
|
+
return typeof message === "string" && message ? message : "Could not load";
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
158
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
159
|
+
const getCommonHeadersMock: MockFunction = getJestMockFunction();
|
|
160
|
+
const apiGetMock: MockFunction = getJestMockFunction();
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
* The arrow wrappers are load bearing: jest.mock is hoisted above the compiled
|
|
164
|
+
* requires, so the consts above are still in their temporal dead zone when the
|
|
165
|
+
* factory body runs. Dereferencing them lazily, at call time, is what works.
|
|
166
|
+
*/
|
|
167
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
168
|
+
return {
|
|
169
|
+
__esModule: true,
|
|
170
|
+
default: {
|
|
171
|
+
getList: (...args: Array<any>) => {
|
|
172
|
+
return getListMock(...args);
|
|
173
|
+
},
|
|
174
|
+
getItem: (...args: Array<any>) => {
|
|
175
|
+
return getItemMock(...args);
|
|
176
|
+
},
|
|
177
|
+
getCommonHeaders: (...args: Array<any>) => {
|
|
178
|
+
return getCommonHeadersMock(...args);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
185
|
+
return {
|
|
186
|
+
__esModule: true,
|
|
187
|
+
default: {
|
|
188
|
+
get: (...args: Array<any>) => {
|
|
189
|
+
return apiGetMock(...args);
|
|
190
|
+
},
|
|
191
|
+
/*
|
|
192
|
+
* Both spellings are used by the page and they are handed different
|
|
193
|
+
* things - the identity read routes an Exception through
|
|
194
|
+
* getFriendlyErrorMessage, the readiness read routes an HTTPErrorResponse
|
|
195
|
+
* through getFriendlyMessage. Reading `.message` off either covers both,
|
|
196
|
+
* because HTTPErrorResponse exposes the server's message under exactly
|
|
197
|
+
* that name, and surfacing the real text rather than a fixed string is
|
|
198
|
+
* what makes an error-state assertion mean anything.
|
|
199
|
+
*/
|
|
200
|
+
getFriendlyMessage: (error: unknown) => {
|
|
201
|
+
return readErrorMessage(error);
|
|
202
|
+
},
|
|
203
|
+
getFriendlyErrorMessage: (error: unknown) => {
|
|
204
|
+
return readErrorMessage(error);
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
jest.mock("react-i18next", () => {
|
|
211
|
+
return {
|
|
212
|
+
useTranslation: () => {
|
|
213
|
+
return {
|
|
214
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
215
|
+
return options?.defaultValue ?? key;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/*
|
|
223
|
+
* A stand-in ModelTable that records the props it was handed. Rendering the real
|
|
224
|
+
* one would drag in the pager, the facet bar and the URL state, none of which
|
|
225
|
+
* this file is about - what matters is which MODEL each table is mounted over,
|
|
226
|
+
* the query it filters on, the row it builds on create, the affordances it is
|
|
227
|
+
* given and the key it stores preferences under.
|
|
228
|
+
*/
|
|
229
|
+
interface CapturedFormField {
|
|
230
|
+
field?: Record<string, true> | undefined;
|
|
231
|
+
overrideField?: Record<string, true> | undefined;
|
|
232
|
+
overrideFieldKey?: string | undefined;
|
|
233
|
+
title?: string | undefined;
|
|
234
|
+
description?: string | undefined;
|
|
235
|
+
dropdownOptions?: Array<{ label: string; value: string }> | undefined;
|
|
236
|
+
doNotShowWhenEditing?: boolean | undefined;
|
|
237
|
+
doNotShowWhenCreating?: boolean | undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface CapturedColumn {
|
|
241
|
+
title: string;
|
|
242
|
+
/*
|
|
243
|
+
* Captured because a column is a REQUEST as well as a renderer: ModelTable
|
|
244
|
+
* unions every column's `field` with `selectMoreFields` to build the
|
|
245
|
+
* projection it sends. A guard applied to only one of the two still puts the
|
|
246
|
+
* other on the wire.
|
|
247
|
+
*/
|
|
248
|
+
field: Record<string, unknown>;
|
|
249
|
+
getElement?: ((item: UserNotificationRule) => ReactElement) | undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface CapturedTableProps {
|
|
253
|
+
modelType: unknown;
|
|
254
|
+
userPreferencesKey: string;
|
|
255
|
+
query: Record<string, ObjectID | NotificationRuleType | undefined>;
|
|
256
|
+
onBeforeCreate: (
|
|
257
|
+
model: UserNotificationRule,
|
|
258
|
+
miscDataProps: Record<string, unknown>,
|
|
259
|
+
) => Promise<UserNotificationRule>;
|
|
260
|
+
isCreateable: boolean;
|
|
261
|
+
isDeleteable: boolean;
|
|
262
|
+
isEditable: boolean;
|
|
263
|
+
createVerb: string;
|
|
264
|
+
sortBy: string;
|
|
265
|
+
sortOrder: SortOrder;
|
|
266
|
+
id: string;
|
|
267
|
+
name: string;
|
|
268
|
+
noItemsMessage: string;
|
|
269
|
+
singularName?: string | undefined;
|
|
270
|
+
selectMoreFields: Record<string, unknown>;
|
|
271
|
+
formFields: Array<CapturedFormField>;
|
|
272
|
+
columns: Array<CapturedColumn>;
|
|
273
|
+
cardProps: { title: string; description: string };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let capturedTables: Array<CapturedTableProps> = [];
|
|
277
|
+
|
|
278
|
+
jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
|
|
279
|
+
return {
|
|
280
|
+
__esModule: true,
|
|
281
|
+
default: (props: CapturedTableProps) => {
|
|
282
|
+
capturedTables.push(props);
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
289
|
+
import UserViewNotificationRules from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/NotificationRules";
|
|
290
|
+
import AlertOnCallRules from "../../../../App/FeatureSet/Dashboard/src/Pages/UserSettings/AlertOnCallRules";
|
|
291
|
+
import EpisodeOnCallRules from "../../../../App/FeatureSet/Dashboard/src/Pages/UserSettings/EpisodeOnCallRules";
|
|
292
|
+
import IncidentEpisodeOnCallRules from "../../../../App/FeatureSet/Dashboard/src/Pages/UserSettings/IncidentEpisodeOnCallRules";
|
|
293
|
+
import IncidentOnCallRules from "../../../../App/FeatureSet/Dashboard/src/Pages/UserSettings/IncidentOnCallRules";
|
|
294
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
295
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
296
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
297
|
+
import TeamMember from "../../../Models/DatabaseModels/TeamMember";
|
|
298
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
299
|
+
import UserCall from "../../../Models/DatabaseModels/UserCall";
|
|
300
|
+
import UserEmail from "../../../Models/DatabaseModels/UserEmail";
|
|
301
|
+
import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
|
|
302
|
+
import UserPush from "../../../Models/DatabaseModels/UserPush";
|
|
303
|
+
import UserSMS from "../../../Models/DatabaseModels/UserSMS";
|
|
304
|
+
import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
|
|
305
|
+
import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
|
|
306
|
+
import UserWhatsApp from "../../../Models/DatabaseModels/UserWhatsApp";
|
|
307
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
308
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
309
|
+
import Route from "../../../Types/API/Route";
|
|
310
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
311
|
+
import Email from "../../../Types/Email";
|
|
312
|
+
import { JSONArray, JSONObject } from "../../../Types/JSON";
|
|
313
|
+
import Name from "../../../Types/Name";
|
|
314
|
+
import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
|
|
315
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
316
|
+
import Permission from "../../../Types/Permission";
|
|
317
|
+
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
318
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
319
|
+
import UserUtil from "../../../UI/Utils/User";
|
|
320
|
+
|
|
321
|
+
const PROJECT_ID: ObjectID = new ObjectID(PROJECT_ID_STRING);
|
|
322
|
+
const SIGNED_IN_USER_ID: ObjectID = new ObjectID(SIGNED_IN_USER_ID_STRING);
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
* The seven models only their owner may read. They are asserted as a SET because
|
|
326
|
+
* which one an admin surface reaches for hardly matters - every one of them
|
|
327
|
+
* carries a raw identifier and most carry a credential.
|
|
328
|
+
*/
|
|
329
|
+
const NOTIFICATION_METHOD_MODELS: Array<unknown> = [
|
|
330
|
+
UserEmail,
|
|
331
|
+
UserSMS,
|
|
332
|
+
UserCall,
|
|
333
|
+
UserPush,
|
|
334
|
+
UserWhatsApp,
|
|
335
|
+
UserTelegram,
|
|
336
|
+
UserWebhook,
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
/*
|
|
340
|
+
* The RELATION spelling of those same seven, as they appear in a nested select
|
|
341
|
+
* on UserNotificationRule. Selecting any of them pulls a column out of the
|
|
342
|
+
* method model itself - the address, the number, the handle - through a table
|
|
343
|
+
* an administrator may read and which is not row-scoped for them. It is the
|
|
344
|
+
* second door into the room NOTIFICATION_METHOD_MODELS guards, and it does not
|
|
345
|
+
* look like a request at all.
|
|
346
|
+
*/
|
|
347
|
+
const METHOD_RELATION_KEYS: Array<string> = [
|
|
348
|
+
"userEmail",
|
|
349
|
+
"userSms",
|
|
350
|
+
"userCall",
|
|
351
|
+
"userPush",
|
|
352
|
+
"userWhatsApp",
|
|
353
|
+
"userTelegram",
|
|
354
|
+
"userWebhook",
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
/* Every foreign key a rule can point a method at. */
|
|
358
|
+
const METHOD_FOREIGN_KEYS: Array<keyof UserNotificationRule> = [
|
|
359
|
+
"userEmailId",
|
|
360
|
+
"userSmsId",
|
|
361
|
+
"userCallId",
|
|
362
|
+
"userPushId",
|
|
363
|
+
"userWhatsAppId",
|
|
364
|
+
"userTelegramId",
|
|
365
|
+
"userWebhookId",
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
type SeverityModelType = { new (): IncidentSeverity | AlertSeverity };
|
|
369
|
+
type SeverityForeignKeyColumn = "incidentSeverityId" | "alertSeverityId";
|
|
370
|
+
|
|
371
|
+
interface SeveritySpec {
|
|
372
|
+
id: string;
|
|
373
|
+
name: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const INCIDENT_SEVERITY_SPECS: Array<SeveritySpec> = [
|
|
377
|
+
{ id: INCIDENT_SEVERITY_ONE_ID, name: INCIDENT_SEVERITY_ONE_NAME },
|
|
378
|
+
{ id: INCIDENT_SEVERITY_TWO_ID, name: INCIDENT_SEVERITY_TWO_NAME },
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
const ALERT_SEVERITY_SPECS: Array<SeveritySpec> = [
|
|
382
|
+
{ id: ALERT_SEVERITY_ONE_ID, name: ALERT_SEVERITY_ONE_NAME },
|
|
383
|
+
{ id: ALERT_SEVERITY_TWO_ID, name: ALERT_SEVERITY_TWO_NAME },
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
type BuildSeverities = (
|
|
387
|
+
modelType: SeverityModelType,
|
|
388
|
+
specs: Array<SeveritySpec>,
|
|
389
|
+
) => Array<IncidentSeverity | AlertSeverity>;
|
|
390
|
+
|
|
391
|
+
const buildSeverities: BuildSeverities = (
|
|
392
|
+
modelType: SeverityModelType,
|
|
393
|
+
specs: Array<SeveritySpec>,
|
|
394
|
+
): Array<IncidentSeverity | AlertSeverity> => {
|
|
395
|
+
return specs.map((spec: SeveritySpec) => {
|
|
396
|
+
const severity: IncidentSeverity | AlertSeverity = new modelType();
|
|
397
|
+
severity._id = spec.id;
|
|
398
|
+
severity.name = spec.name;
|
|
399
|
+
return severity;
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/*
|
|
404
|
+
* ------------------------------------------------------------------ *
|
|
405
|
+
* The readiness payload
|
|
406
|
+
* ------------------------------------------------------------------
|
|
407
|
+
*/
|
|
408
|
+
|
|
409
|
+
interface MethodSpec {
|
|
410
|
+
methodId: string;
|
|
411
|
+
methodType: string;
|
|
412
|
+
maskedIdentifier: string;
|
|
413
|
+
isVerified: boolean;
|
|
414
|
+
// The value a leaky server - or a leaky future refactor - would hand over.
|
|
415
|
+
leakedRawValue: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
type MethodJsonFunction = (spec: MethodSpec) => JSONObject;
|
|
419
|
+
|
|
420
|
+
const methodJson: MethodJsonFunction = (spec: MethodSpec): JSONObject => {
|
|
421
|
+
return {
|
|
422
|
+
methodId: spec.methodId,
|
|
423
|
+
methodType: spec.methodType,
|
|
424
|
+
maskedIdentifier: spec.maskedIdentifier,
|
|
425
|
+
isVerified: spec.isVerified,
|
|
426
|
+
/*
|
|
427
|
+
* Three plausible spellings of the same mistake, planted at once. None is
|
|
428
|
+
* part of the frozen contract, so the parse drops all three and nothing
|
|
429
|
+
* downstream has an unmasked value available to render by accident.
|
|
430
|
+
*/
|
|
431
|
+
identifier: spec.leakedRawValue,
|
|
432
|
+
rawIdentifier: spec.leakedRawValue,
|
|
433
|
+
webhookUrl: RAW_WEBHOOK_URL,
|
|
434
|
+
};
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const VERIFIED_EMAIL_METHOD: JSONObject = methodJson({
|
|
438
|
+
methodId: EMAIL_METHOD_ID,
|
|
439
|
+
methodType: "Email",
|
|
440
|
+
maskedIdentifier: MASKED_EMAIL,
|
|
441
|
+
isVerified: true,
|
|
442
|
+
leakedRawValue: RAW_EMAIL,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const VERIFIED_SMS_METHOD: JSONObject = methodJson({
|
|
446
|
+
methodId: SMS_METHOD_ID,
|
|
447
|
+
methodType: "SMS",
|
|
448
|
+
maskedIdentifier: MASKED_PHONE,
|
|
449
|
+
isVerified: true,
|
|
450
|
+
leakedRawValue: RAW_PHONE,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const UNVERIFIED_TELEGRAM_METHOD: JSONObject = methodJson({
|
|
454
|
+
methodId: TELEGRAM_METHOD_ID,
|
|
455
|
+
methodType: "Telegram",
|
|
456
|
+
maskedIdentifier: MASKED_TELEGRAM,
|
|
457
|
+
isVerified: false,
|
|
458
|
+
leakedRawValue: RAW_TELEGRAM,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
* A coverage list with a hole in it, banded by the same disjoint severity ids
|
|
463
|
+
* the tables use, so the grid on screen and the tables underneath are talking
|
|
464
|
+
* about the same project.
|
|
465
|
+
*/
|
|
466
|
+
const COVERAGE: JSONArray = [
|
|
467
|
+
{
|
|
468
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
469
|
+
severityId: INCIDENT_SEVERITY_ONE_ID,
|
|
470
|
+
severityName: INCIDENT_SEVERITY_ONE_NAME,
|
|
471
|
+
hasRule: true,
|
|
472
|
+
isOptOut: null,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
476
|
+
severityId: INCIDENT_SEVERITY_TWO_ID,
|
|
477
|
+
severityName: INCIDENT_SEVERITY_TWO_NAME,
|
|
478
|
+
hasRule: false,
|
|
479
|
+
isOptOut: null,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
483
|
+
severityId: ALERT_SEVERITY_ONE_ID,
|
|
484
|
+
severityName: ALERT_SEVERITY_ONE_NAME,
|
|
485
|
+
hasRule: true,
|
|
486
|
+
isOptOut: null,
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
|
|
490
|
+
hasRule: true,
|
|
491
|
+
isOptOut: null,
|
|
492
|
+
},
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
interface ReadinessOptions {
|
|
496
|
+
status?: string | undefined;
|
|
497
|
+
methods?: JSONArray | undefined;
|
|
498
|
+
reasons?: Array<string> | undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
type ReadinessJsonFunction = (options?: ReadinessOptions) => JSONObject;
|
|
502
|
+
|
|
503
|
+
/*
|
|
504
|
+
* The per-user endpoint returns ONE user, not a summary - the page wraps it as
|
|
505
|
+
* the one-element `users` list the shared parser expects, which is the whole
|
|
506
|
+
* reason this fixture is a bare user object rather than a summary.
|
|
507
|
+
*/
|
|
508
|
+
const readinessJson: ReadinessJsonFunction = (
|
|
509
|
+
options: ReadinessOptions = {},
|
|
510
|
+
): JSONObject => {
|
|
511
|
+
return {
|
|
512
|
+
userId: TARGET_USER_ID_STRING,
|
|
513
|
+
userName: TARGET_USER_NAME,
|
|
514
|
+
userEmail: TARGET_LOGIN_EMAIL,
|
|
515
|
+
status: options.status ?? "PartiallyReady",
|
|
516
|
+
methods: options.methods ?? [
|
|
517
|
+
VERIFIED_EMAIL_METHOD,
|
|
518
|
+
VERIFIED_SMS_METHOD,
|
|
519
|
+
UNVERIFIED_TELEGRAM_METHOD,
|
|
520
|
+
],
|
|
521
|
+
coverage: COVERAGE,
|
|
522
|
+
reasons: options.reasons ?? [
|
|
523
|
+
`No notification rule for incidents at ${INCIDENT_SEVERITY_TWO_NAME}.`,
|
|
524
|
+
],
|
|
525
|
+
reachedVia: ["Team"],
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const NO_METHODS_READINESS: JSONObject = readinessJson({
|
|
530
|
+
status: "NotReachable",
|
|
531
|
+
methods: [],
|
|
532
|
+
reasons: ["No verified notification method on this account."],
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
/*
|
|
536
|
+
* ------------------------------------------------------------------ *
|
|
537
|
+
* Wiring
|
|
538
|
+
* ------------------------------------------------------------------
|
|
539
|
+
*/
|
|
540
|
+
|
|
541
|
+
type RespondWithReadinessFunction = (payload: JSONObject) => void;
|
|
542
|
+
|
|
543
|
+
const respondWithReadiness: RespondWithReadinessFunction = (
|
|
544
|
+
payload: JSONObject,
|
|
545
|
+
): void => {
|
|
546
|
+
apiGetMock.mockResolvedValue(
|
|
547
|
+
new HTTPResponse<JSONObject>(200, payload, {}) as never,
|
|
548
|
+
);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
type SetUpListsFunction = (teamMembers: Array<TeamMember>) => void;
|
|
552
|
+
|
|
553
|
+
const setUpLists: SetUpListsFunction = (
|
|
554
|
+
teamMembers: Array<TeamMember>,
|
|
555
|
+
): void => {
|
|
556
|
+
getListMock.mockImplementation((data: any) => {
|
|
557
|
+
if (data.modelType === IncidentSeverity) {
|
|
558
|
+
return Promise.resolve({
|
|
559
|
+
data: buildSeverities(IncidentSeverity, INCIDENT_SEVERITY_SPECS),
|
|
560
|
+
count: INCIDENT_SEVERITY_SPECS.length,
|
|
561
|
+
skip: 0,
|
|
562
|
+
limit: INCIDENT_SEVERITY_SPECS.length,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (data.modelType === AlertSeverity) {
|
|
567
|
+
return Promise.resolve({
|
|
568
|
+
data: buildSeverities(AlertSeverity, ALERT_SEVERITY_SPECS),
|
|
569
|
+
count: ALERT_SEVERITY_SPECS.length,
|
|
570
|
+
skip: 0,
|
|
571
|
+
limit: ALERT_SEVERITY_SPECS.length,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (data.modelType === TeamMember) {
|
|
576
|
+
return Promise.resolve({
|
|
577
|
+
data: teamMembers,
|
|
578
|
+
count: teamMembers.length,
|
|
579
|
+
skip: 0,
|
|
580
|
+
limit: 1,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/*
|
|
585
|
+
* Anything else, INCLUDING the seven notification-method models - which this
|
|
586
|
+
* page must never ask for. A non-empty answer here would make a leak look
|
|
587
|
+
* like a feature working, so the fallback stays empty and the assertion that
|
|
588
|
+
* matters is about the request rather than the response: see "never reads a
|
|
589
|
+
* notification method model" below.
|
|
590
|
+
*/
|
|
591
|
+
return Promise.resolve({ data: [], count: 0, skip: 0, limit: 0 });
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
type BuildTeamMemberFunction = () => TeamMember;
|
|
596
|
+
|
|
597
|
+
const buildTeamMember: BuildTeamMemberFunction = (): TeamMember => {
|
|
598
|
+
const user: User = new User();
|
|
599
|
+
user._id = TARGET_USER_ID_STRING;
|
|
600
|
+
user.name = new Name(TARGET_USER_NAME);
|
|
601
|
+
user.email = new Email(TARGET_LOGIN_EMAIL);
|
|
602
|
+
|
|
603
|
+
const member: TeamMember = new TeamMember();
|
|
604
|
+
member.user = user;
|
|
605
|
+
|
|
606
|
+
return member;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const pageProps: PageComponentProps = {
|
|
610
|
+
pageRoute: new Route("/users/notification-rules"),
|
|
611
|
+
currentProject: null,
|
|
612
|
+
hasPaymentMethod: false,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
type RenderAdminPageFunction = (
|
|
616
|
+
targetUserId?: string | undefined,
|
|
617
|
+
) => Promise<HTMLElement>;
|
|
618
|
+
|
|
619
|
+
/*
|
|
620
|
+
* The user id is read off the URL at offset 1 - this is a sub-page, so the last
|
|
621
|
+
* segment is "notification-rules" and the id is the one before it. Driving that
|
|
622
|
+
* through a real jsdom URL rather than a stub is deliberate: reading the wrong
|
|
623
|
+
* offset yields an ObjectID of the literal string "notification-rules", which
|
|
624
|
+
* every assertion about whose rules these are would then fail on.
|
|
625
|
+
*/
|
|
626
|
+
const renderAdminPage: RenderAdminPageFunction = async (
|
|
627
|
+
targetUserId: string = TARGET_USER_ID_STRING,
|
|
628
|
+
): Promise<HTMLElement> => {
|
|
629
|
+
window.history.pushState(
|
|
630
|
+
{},
|
|
631
|
+
"",
|
|
632
|
+
`/dashboard/${PROJECT_ID_STRING}/users/${targetUserId}/notification-rules`,
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const { container } = render(<UserViewNotificationRules {...pageProps} />);
|
|
636
|
+
|
|
637
|
+
await waitFor((): void => {
|
|
638
|
+
expect(apiGetMock).toHaveBeenCalled();
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return container;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
type RenderAdminPageWithTablesFunction = () => Promise<HTMLElement>;
|
|
645
|
+
|
|
646
|
+
const renderAdminPageWithTables: RenderAdminPageWithTablesFunction =
|
|
647
|
+
async (): Promise<HTMLElement> => {
|
|
648
|
+
const container: HTMLElement = await renderAdminPage();
|
|
649
|
+
|
|
650
|
+
await waitFor((): void => {
|
|
651
|
+
expect(capturedTables).toHaveLength(8);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
return container;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
type RenderSelfServePageFunction = (
|
|
658
|
+
Page: FunctionComponent<PageComponentProps>,
|
|
659
|
+
) => Promise<void>;
|
|
660
|
+
|
|
661
|
+
const renderSelfServePage: RenderSelfServePageFunction = async (
|
|
662
|
+
Page: FunctionComponent<PageComponentProps>,
|
|
663
|
+
): Promise<void> => {
|
|
664
|
+
render(<Page {...pageProps} />);
|
|
665
|
+
|
|
666
|
+
await waitFor((): void => {
|
|
667
|
+
expect(capturedTables).toHaveLength(2);
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
type TablesForFunction = (
|
|
672
|
+
ruleType: NotificationRuleType,
|
|
673
|
+
) => Array<CapturedTableProps>;
|
|
674
|
+
|
|
675
|
+
/*
|
|
676
|
+
* Four components mount at once and each resolves its own fetches, so the order
|
|
677
|
+
* capturedTables ends up in is not something to assert against. The rule type on
|
|
678
|
+
* the query is the stable handle.
|
|
679
|
+
*/
|
|
680
|
+
const tablesFor: TablesForFunction = (
|
|
681
|
+
ruleType: NotificationRuleType,
|
|
682
|
+
): Array<CapturedTableProps> => {
|
|
683
|
+
return capturedTables.filter((table: CapturedTableProps) => {
|
|
684
|
+
return table.query["ruleType"] === ruleType;
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
type GetCapturedTablesFunction = () => Array<CapturedTableProps>;
|
|
689
|
+
|
|
690
|
+
/*
|
|
691
|
+
* The captured list is REPLACED between tests rather than emptied in place, so
|
|
692
|
+
* a closure built inside the `for` loops below would otherwise read whichever
|
|
693
|
+
* array happened to be bound when the loop ran. Going through a stable function
|
|
694
|
+
* makes each test see the list as it stands when that test executes, which is
|
|
695
|
+
* also what stops eslint's no-loop-func from being right about it.
|
|
696
|
+
*/
|
|
697
|
+
const getCapturedTables: GetCapturedTablesFunction =
|
|
698
|
+
(): Array<CapturedTableProps> => {
|
|
699
|
+
return capturedTables;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
/*
|
|
703
|
+
* The most recent props each table was rendered with, keyed by the preferences
|
|
704
|
+
* key that already uniquely identifies a table on this page.
|
|
705
|
+
*
|
|
706
|
+
* The page renders its tables as soon as it knows WHO it is about and re-renders
|
|
707
|
+
* them when readiness lands, so `capturedTables` legitimately holds two entries
|
|
708
|
+
* per table. Assertions about copy that depends on the readiness payload - the
|
|
709
|
+
* empty-state sentence, which changes sign with the project's fallback switch -
|
|
710
|
+
* have to read the settled render rather than whichever one happened to be
|
|
711
|
+
* first.
|
|
712
|
+
*/
|
|
713
|
+
type LatestTablesFunction = () => Array<CapturedTableProps>;
|
|
714
|
+
|
|
715
|
+
const latestTables: LatestTablesFunction = (): Array<CapturedTableProps> => {
|
|
716
|
+
const byKey: Map<string, CapturedTableProps> = new Map<
|
|
717
|
+
string,
|
|
718
|
+
CapturedTableProps
|
|
719
|
+
>();
|
|
720
|
+
|
|
721
|
+
for (const table of capturedTables) {
|
|
722
|
+
byKey.set(table.userPreferencesKey, table);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return [...byKey.values()];
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
type FormFieldForFunction = (
|
|
729
|
+
table: CapturedTableProps,
|
|
730
|
+
key: string,
|
|
731
|
+
) => CapturedFormField;
|
|
732
|
+
|
|
733
|
+
/*
|
|
734
|
+
* A form field by the column (or override key) it writes. Which columns the
|
|
735
|
+
* form offers is the whole question on the edit path - the server decides what
|
|
736
|
+
* it will accept from the column access control, and a field for anything else
|
|
737
|
+
* is a control that is refused or, worse, silently dropped.
|
|
738
|
+
*/
|
|
739
|
+
const formFieldFor: FormFieldForFunction = (
|
|
740
|
+
table: CapturedTableProps,
|
|
741
|
+
key: string,
|
|
742
|
+
): CapturedFormField => {
|
|
743
|
+
const field: CapturedFormField | undefined = table.formFields.find(
|
|
744
|
+
(candidate: CapturedFormField): boolean => {
|
|
745
|
+
return Boolean(
|
|
746
|
+
candidate.field?.[key] ||
|
|
747
|
+
candidate.overrideField?.[key] ||
|
|
748
|
+
candidate.overrideFieldKey === key,
|
|
749
|
+
);
|
|
750
|
+
},
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
if (!field) {
|
|
754
|
+
throw new Error(`no form field for ${key}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return field;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
type FormFieldKeysFunction = (table: CapturedTableProps) => Array<string>;
|
|
761
|
+
|
|
762
|
+
const formFieldKeys: FormFieldKeysFunction = (
|
|
763
|
+
table: CapturedTableProps,
|
|
764
|
+
): Array<string> => {
|
|
765
|
+
return table.formFields.map((field: CapturedFormField): string => {
|
|
766
|
+
return (
|
|
767
|
+
field.overrideFieldKey ||
|
|
768
|
+
Object.keys(field.field || field.overrideField || {})[0] ||
|
|
769
|
+
""
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
type MethodOptionsForFunction = (
|
|
775
|
+
table: CapturedTableProps,
|
|
776
|
+
) => Array<{ label: string; value: string }>;
|
|
777
|
+
|
|
778
|
+
/*
|
|
779
|
+
* What the create form's method dropdown is offering. This is the surface the
|
|
780
|
+
* widening was done FOR, so it is the one that has to be shown working without
|
|
781
|
+
* it: masked labels, real ids, and not one request for a method row.
|
|
782
|
+
*/
|
|
783
|
+
const methodOptionsFor: MethodOptionsForFunction = (
|
|
784
|
+
table: CapturedTableProps,
|
|
785
|
+
): Array<{ label: string; value: string }> => {
|
|
786
|
+
return formFieldFor(table, "notificationMethod").dropdownOptions || [];
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
type MethodColumnsSetFunction = (
|
|
790
|
+
rule: UserNotificationRule,
|
|
791
|
+
) => Array<{ column: string; value: string }>;
|
|
792
|
+
|
|
793
|
+
const methodColumnsSet: MethodColumnsSetFunction = (
|
|
794
|
+
rule: UserNotificationRule,
|
|
795
|
+
): Array<{ column: string; value: string }> => {
|
|
796
|
+
const columns: Array<{ column: string; value: string }> = [];
|
|
797
|
+
|
|
798
|
+
for (const foreignKey of METHOD_FOREIGN_KEYS) {
|
|
799
|
+
const value: unknown = rule[foreignKey];
|
|
800
|
+
|
|
801
|
+
if (value) {
|
|
802
|
+
columns.push({
|
|
803
|
+
column: foreignKey as string,
|
|
804
|
+
value: (value as ObjectID).toString(),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return columns;
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
type RequestedModelTypesFunction = () => Array<unknown>;
|
|
813
|
+
|
|
814
|
+
const requestedModelTypes: RequestedModelTypesFunction = (): Array<unknown> => {
|
|
815
|
+
return getListMock.mock.calls.map((call: Array<any>) => {
|
|
816
|
+
return call[0].modelType;
|
|
817
|
+
});
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
type ColumnNamedFunction = (
|
|
821
|
+
table: CapturedTableProps,
|
|
822
|
+
title: string,
|
|
823
|
+
) => CapturedColumn;
|
|
824
|
+
|
|
825
|
+
const columnNamed: ColumnNamedFunction = (
|
|
826
|
+
table: CapturedTableProps,
|
|
827
|
+
title: string,
|
|
828
|
+
): CapturedColumn => {
|
|
829
|
+
const column: CapturedColumn | undefined = table.columns.find(
|
|
830
|
+
(candidate: CapturedColumn): boolean => {
|
|
831
|
+
return candidate.title === title;
|
|
832
|
+
},
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
if (!column) {
|
|
836
|
+
throw new Error(`no column titled ${title}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return column;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
type MatrixForFunction = (label: string) => HTMLElement;
|
|
843
|
+
|
|
844
|
+
const matrixFor: MatrixForFunction = (label: string): HTMLElement => {
|
|
845
|
+
return screen.getByRole("table", { name: `${label} coverage` });
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
type CardNamedFunction = (title: string) => HTMLElement;
|
|
849
|
+
|
|
850
|
+
const cardNamed: CardNamedFunction = (title: string): HTMLElement => {
|
|
851
|
+
const card: HTMLElement | null = screen
|
|
852
|
+
.getByText(title)
|
|
853
|
+
.closest('[data-testid="card"]') as HTMLElement | null;
|
|
854
|
+
|
|
855
|
+
if (!card) {
|
|
856
|
+
throw new Error(`no card titled ${title}`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return card;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
type MailtoHrefInFunction = (element: HTMLElement) => string;
|
|
863
|
+
|
|
864
|
+
const mailtoHrefIn: MailtoHrefInFunction = (element: HTMLElement): string => {
|
|
865
|
+
const href: string | null | undefined = element
|
|
866
|
+
.querySelector('a[href^="mailto:"]')
|
|
867
|
+
?.getAttribute("href");
|
|
868
|
+
|
|
869
|
+
if (!href) {
|
|
870
|
+
throw new Error("no mailto link");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return decodeURIComponent(href);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/*
|
|
877
|
+
* ------------------------------------------------------------------ *
|
|
878
|
+
* The four rule types, stated as two independent axes
|
|
879
|
+
* ------------------------------------------------------------------
|
|
880
|
+
*/
|
|
881
|
+
|
|
882
|
+
interface RuleTypeCase {
|
|
883
|
+
label: string;
|
|
884
|
+
ruleType: NotificationRuleType;
|
|
885
|
+
Page: FunctionComponent<PageComponentProps>;
|
|
886
|
+
severityModelType: SeverityModelType;
|
|
887
|
+
wrongSeverityModelType: SeverityModelType;
|
|
888
|
+
foreignKeyColumn: SeverityForeignKeyColumn;
|
|
889
|
+
wrongForeignKeyColumn: SeverityForeignKeyColumn;
|
|
890
|
+
severitySpecs: Array<SeveritySpec>;
|
|
891
|
+
/* The pre-extraction card copy, reproduced from the deleted pages. */
|
|
892
|
+
getTitle: (severityName: string) => string;
|
|
893
|
+
getDescription: (severityName: string) => string;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/*
|
|
897
|
+
* Note the crossed pair: the ALERT episode page reads AlertSeverity while the
|
|
898
|
+
* INCIDENT episode page reads IncidentSeverity. That crossing is the whole
|
|
899
|
+
* reason the severity model and the foreign key column are two separate props,
|
|
900
|
+
* and a case table is the only shape in which an accidental re-derivation of one
|
|
901
|
+
* from the other fails on exactly one row rather than passing on all four.
|
|
902
|
+
*/
|
|
903
|
+
const RULE_TYPE_CASES: Array<RuleTypeCase> = [
|
|
904
|
+
{
|
|
905
|
+
label: "incident",
|
|
906
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
907
|
+
Page: IncidentOnCallRules,
|
|
908
|
+
severityModelType: IncidentSeverity,
|
|
909
|
+
wrongSeverityModelType: AlertSeverity,
|
|
910
|
+
foreignKeyColumn: "incidentSeverityId",
|
|
911
|
+
wrongForeignKeyColumn: "alertSeverityId",
|
|
912
|
+
severitySpecs: INCIDENT_SEVERITY_SPECS,
|
|
913
|
+
getTitle: (severityName: string): string => {
|
|
914
|
+
return `${severityName} Severity: When I am on call and ${severityName} is assigned to me...`;
|
|
915
|
+
},
|
|
916
|
+
getDescription: (severityName: string): string => {
|
|
917
|
+
return `Here are the rules when you are on call and ${severityName} is assigned to you.`;
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
label: "incident episode",
|
|
922
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
923
|
+
Page: IncidentEpisodeOnCallRules,
|
|
924
|
+
severityModelType: IncidentSeverity,
|
|
925
|
+
wrongSeverityModelType: AlertSeverity,
|
|
926
|
+
foreignKeyColumn: "incidentSeverityId",
|
|
927
|
+
wrongForeignKeyColumn: "alertSeverityId",
|
|
928
|
+
severitySpecs: INCIDENT_SEVERITY_SPECS,
|
|
929
|
+
getTitle: (severityName: string): string => {
|
|
930
|
+
return `${severityName} Severity Episode: When I am on call and ${severityName} severity episode is assigned to me...`;
|
|
931
|
+
},
|
|
932
|
+
getDescription: (severityName: string): string => {
|
|
933
|
+
return `Here are the rules when you are on call and ${severityName} Severity episode is assigned to you.`;
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
label: "alert",
|
|
938
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
939
|
+
Page: AlertOnCallRules,
|
|
940
|
+
severityModelType: AlertSeverity,
|
|
941
|
+
wrongSeverityModelType: IncidentSeverity,
|
|
942
|
+
foreignKeyColumn: "alertSeverityId",
|
|
943
|
+
wrongForeignKeyColumn: "incidentSeverityId",
|
|
944
|
+
severitySpecs: ALERT_SEVERITY_SPECS,
|
|
945
|
+
getTitle: (severityName: string): string => {
|
|
946
|
+
return `${severityName} Severity Alert: When I am on call and ${severityName} severity alert is assigned to me...`;
|
|
947
|
+
},
|
|
948
|
+
getDescription: (severityName: string): string => {
|
|
949
|
+
return `Here are the rules when you are on call and ${severityName} Severity alert is assigned to you.`;
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
label: "alert episode",
|
|
954
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
|
955
|
+
Page: EpisodeOnCallRules,
|
|
956
|
+
severityModelType: AlertSeverity,
|
|
957
|
+
wrongSeverityModelType: IncidentSeverity,
|
|
958
|
+
foreignKeyColumn: "alertSeverityId",
|
|
959
|
+
wrongForeignKeyColumn: "incidentSeverityId",
|
|
960
|
+
severitySpecs: ALERT_SEVERITY_SPECS,
|
|
961
|
+
getTitle: (severityName: string): string => {
|
|
962
|
+
return `${severityName} Severity Episode: When I am on call and ${severityName} severity episode is assigned to me...`;
|
|
963
|
+
},
|
|
964
|
+
getDescription: (severityName: string): string => {
|
|
965
|
+
return `Here are the rules when you are on call and ${severityName} Severity episode is assigned to you.`;
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
];
|
|
969
|
+
|
|
970
|
+
const ADMIN_PREFERENCES_PREFIX: string = "admin-user-notification-rules";
|
|
971
|
+
const SELF_SERVE_PREFERENCES_PREFIX: string = "user-notification-rules-table";
|
|
972
|
+
|
|
973
|
+
const NO_ITEMS_MESSAGE: string =
|
|
974
|
+
"No notification rules found for this user. Please add one to receive notifications.";
|
|
975
|
+
|
|
976
|
+
beforeEach((): void => {
|
|
977
|
+
capturedTables = [];
|
|
978
|
+
|
|
979
|
+
getListMock.mockReset();
|
|
980
|
+
getItemMock.mockReset();
|
|
981
|
+
getCommonHeadersMock.mockReset();
|
|
982
|
+
apiGetMock.mockReset();
|
|
983
|
+
|
|
984
|
+
setUpLists([buildTeamMember()]);
|
|
985
|
+
|
|
986
|
+
const project: Project = new Project();
|
|
987
|
+
project.disableOnCallNotificationFallback = false;
|
|
988
|
+
getItemMock.mockResolvedValue(project as never);
|
|
989
|
+
|
|
990
|
+
getCommonHeadersMock.mockReturnValue({} as never);
|
|
991
|
+
respondWithReadiness(readinessJson());
|
|
992
|
+
|
|
993
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
994
|
+
jest.spyOn(UserUtil, "getUserId").mockReturnValue(SIGNED_IN_USER_ID);
|
|
995
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
|
|
996
|
+
jest
|
|
997
|
+
.spyOn(PermissionUtil, "getAllPermissions")
|
|
998
|
+
.mockReturnValue([Permission.ProjectAdmin]);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
afterEach((): void => {
|
|
1002
|
+
cleanup();
|
|
1003
|
+
jest.restoreAllMocks();
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
describe("the shared rules table, as the admin page wires it", () => {
|
|
1007
|
+
for (const testCase of RULE_TYPE_CASES) {
|
|
1008
|
+
test(`${testCase.label} rules are banded by their own severity model and foreign key column`, async () => {
|
|
1009
|
+
await renderAdminPageWithTables();
|
|
1010
|
+
|
|
1011
|
+
const tables: Array<CapturedTableProps> = tablesFor(testCase.ruleType);
|
|
1012
|
+
|
|
1013
|
+
expect(tables).toHaveLength(testCase.severitySpecs.length);
|
|
1014
|
+
|
|
1015
|
+
tables.forEach((table: CapturedTableProps, index: number) => {
|
|
1016
|
+
/*
|
|
1017
|
+
* The id proves BOTH axes at once. The two severity models return
|
|
1018
|
+
* disjoint id sets, so a table that enumerated the other model would
|
|
1019
|
+
* carry an id from the other set even with the right column name, and a
|
|
1020
|
+
* table that wrote the other column would not carry the id at all.
|
|
1021
|
+
*/
|
|
1022
|
+
expect(table.query[testCase.foreignKeyColumn]?.toString()).toBe(
|
|
1023
|
+
testCase.severitySpecs[index]!.id,
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
/*
|
|
1027
|
+
* Absent, not merely undefined. A query that carries the other column at
|
|
1028
|
+
* all is the shape that reads as "no severity filter" once serialised
|
|
1029
|
+
* onto the wire, and a table with no severity filter lists every
|
|
1030
|
+
* severity's rules - a Sev 4 rule shown, and edited, as if it were Sev 1.
|
|
1031
|
+
*/
|
|
1032
|
+
expect(Object.keys(table.query)).not.toContain(
|
|
1033
|
+
testCase.wrongForeignKeyColumn,
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
expect(table.query["projectId"]?.toString()).toBe(PROJECT_ID_STRING);
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
test(`${testCase.label} rules stamp the same column on a newly created rule`, async () => {
|
|
1041
|
+
await renderAdminPageWithTables();
|
|
1042
|
+
|
|
1043
|
+
const table: CapturedTableProps = tablesFor(testCase.ruleType)[0]!;
|
|
1044
|
+
|
|
1045
|
+
const created: UserNotificationRule = await table.onBeforeCreate(
|
|
1046
|
+
new UserNotificationRule(),
|
|
1047
|
+
{},
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
expect(created.ruleType).toBe(testCase.ruleType);
|
|
1051
|
+
expect(created.projectId?.toString()).toBe(PROJECT_ID_STRING);
|
|
1052
|
+
|
|
1053
|
+
/*
|
|
1054
|
+
* The created row has to land in the table that created it. A rule
|
|
1055
|
+
* stamped with one column while the table filters on the other simply
|
|
1056
|
+
* vanishes from the surface that made it, and the admin adds it again.
|
|
1057
|
+
*/
|
|
1058
|
+
expect(created[testCase.foreignKeyColumn]?.toString()).toBe(
|
|
1059
|
+
testCase.severitySpecs[0]!.id,
|
|
1060
|
+
);
|
|
1061
|
+
expect(created[testCase.wrongForeignKeyColumn]).toBeUndefined();
|
|
1062
|
+
|
|
1063
|
+
// Never the admin's own id. This is the paging hijack in one line.
|
|
1064
|
+
expect(created.userId?.toString()).toBe(TARGET_USER_ID_STRING);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
test("every table is scoped to the user in the URL, never to the signed-in admin", async () => {
|
|
1069
|
+
await renderAdminPageWithTables();
|
|
1070
|
+
|
|
1071
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1072
|
+
expect(table.query["userId"]?.toString()).toBe(TARGET_USER_ID_STRING);
|
|
1073
|
+
expect(table.query["userId"]?.toString()).not.toBe(
|
|
1074
|
+
SIGNED_IN_USER_ID_STRING,
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
/*
|
|
1079
|
+
* And nothing this page reads is scoped to the ADMIN instead. A read of the
|
|
1080
|
+
* signed-in user's own rows would be the shape of a component that had
|
|
1081
|
+
* quietly fallen back to "well, load MY methods then" - which is the paging
|
|
1082
|
+
* hijack this phase guards against, offered as a convenience.
|
|
1083
|
+
*/
|
|
1084
|
+
const queriedUserIds: Array<string | undefined> =
|
|
1085
|
+
getListMock.mock.calls.map((call: Array<any>) => {
|
|
1086
|
+
return call[0].query?.userId?.toString();
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
expect(queriedUserIds).not.toContain(SIGNED_IN_USER_ID_STRING);
|
|
1090
|
+
expect(queriedUserIds).toContain(TARGET_USER_ID_STRING);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
test("never reads a notification method model", async () => {
|
|
1094
|
+
await renderAdminPageWithTables();
|
|
1095
|
+
|
|
1096
|
+
/*
|
|
1097
|
+
* The redesign, in one assertion.
|
|
1098
|
+
*
|
|
1099
|
+
* These seven models are readable only by the person who owns the row, and
|
|
1100
|
+
* that is the whole protection: the table scope pins every read to the
|
|
1101
|
+
* caller. Widening it so this page could fill a dropdown exposed the raw
|
|
1102
|
+
* email and phone, the webhook url, the push device token, the telegram chat
|
|
1103
|
+
* id and the verification code of every member, and no column-level guard
|
|
1104
|
+
* held - it could not see nested relation selects, could not see `query`,
|
|
1105
|
+
* and could not see columns injected after it ran.
|
|
1106
|
+
*
|
|
1107
|
+
* This asserts the REQUEST, not the render. A page that asks and is refused
|
|
1108
|
+
* has still been written on the assumption that it may ask, and the next
|
|
1109
|
+
* person to widen the scope makes it work.
|
|
1110
|
+
*/
|
|
1111
|
+
const requested: Array<unknown> = requestedModelTypes();
|
|
1112
|
+
|
|
1113
|
+
for (const methodModel of NOTIFICATION_METHOD_MODELS) {
|
|
1114
|
+
expect(requested).not.toContain(methodModel);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Not vacuous: it does read, just not those.
|
|
1118
|
+
expect(requested).toContain(TeamMember);
|
|
1119
|
+
expect(requested).toContain(IncidentSeverity);
|
|
1120
|
+
expect(requested).toContain(AlertSeverity);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("asks the server for no column of a method model either", async () => {
|
|
1124
|
+
await renderAdminPageWithTables();
|
|
1125
|
+
|
|
1126
|
+
/*
|
|
1127
|
+
* The same rule as the test above, applied to the OTHER way this page could
|
|
1128
|
+
* reach a method row - and the way it actually did.
|
|
1129
|
+
*
|
|
1130
|
+
* `requestedModelTypes` catches a direct getList over UserSMS. It does not
|
|
1131
|
+
* catch a nested relation select on the RULE, which is what
|
|
1132
|
+
* NotificationMethodUtil.getSelectForNotificationMethods produces
|
|
1133
|
+
* (`{ userSms: { phone: true } }`) and what this table used to ask for in
|
|
1134
|
+
* both its `selectMoreFields` and its method column. That select reaches the
|
|
1135
|
+
* raw phone number through UserNotificationRule - a table an administrator
|
|
1136
|
+
* IS allowed to read, and which is deliberately not row-scoped for them -
|
|
1137
|
+
* and the permission machinery permits it at every step: the nested key is
|
|
1138
|
+
* checked against the rule's own `userSms` column, whose read list is
|
|
1139
|
+
* [Permission.CurrentUser] and CurrentUser is auto-granted to every
|
|
1140
|
+
* authenticated caller; then `canReadOnRelationQuery: true` on UserSMS.phone
|
|
1141
|
+
* short-circuits the related model's own read list entirely.
|
|
1142
|
+
*
|
|
1143
|
+
* So the readiness card masked every identifier and the table one card below
|
|
1144
|
+
* printed them in full.
|
|
1145
|
+
*/
|
|
1146
|
+
for (const table of latestTables()) {
|
|
1147
|
+
const projection: Array<string> = Object.keys(table.selectMoreFields);
|
|
1148
|
+
|
|
1149
|
+
for (const column of table.columns) {
|
|
1150
|
+
projection.push(...Object.keys(column.field || {}));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
for (const relation of METHOD_RELATION_KEYS) {
|
|
1154
|
+
expect(projection).not.toContain(relation);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/*
|
|
1158
|
+
* The ids instead - which carry nothing, are admin-readable by design, and
|
|
1159
|
+
* are what correlate a rule with the masked readiness entry.
|
|
1160
|
+
*/
|
|
1161
|
+
expect(projection).toContain("userSmsId");
|
|
1162
|
+
expect(projection).toContain("userEmailId");
|
|
1163
|
+
expect(projection).toContain("isOptOut");
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
test("labels a listed rule with the masked identifier, not the raw one", async () => {
|
|
1168
|
+
await renderAdminPageWithTables();
|
|
1169
|
+
|
|
1170
|
+
const methodColumn: CapturedColumn = columnNamed(
|
|
1171
|
+
latestTables()[0]!,
|
|
1172
|
+
"Notification Method",
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
const rule: UserNotificationRule = new UserNotificationRule();
|
|
1176
|
+
rule.userSmsId = new ObjectID(SMS_METHOD_ID);
|
|
1177
|
+
rule.notifyAfterMinutes = 5;
|
|
1178
|
+
|
|
1179
|
+
const { container } = render(methodColumn.getElement!(rule));
|
|
1180
|
+
|
|
1181
|
+
/*
|
|
1182
|
+
* End to end: the page never read UserSMS, yet the cell still names the
|
|
1183
|
+
* method the rule pages - because the id it DID read is matched back onto
|
|
1184
|
+
* the masked readiness entry. That is the whole mechanism in one row.
|
|
1185
|
+
*/
|
|
1186
|
+
expect(container.textContent).toContain(MASKED_PHONE);
|
|
1187
|
+
expect(container.textContent).not.toContain(RAW_PHONE);
|
|
1188
|
+
expect(container.textContent).not.toMatch(UNMASKED_PHONE_PATTERN);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
test("builds the rule form's method dropdown out of the masked readiness payload", async () => {
|
|
1192
|
+
await renderAdminPageWithTables();
|
|
1193
|
+
|
|
1194
|
+
latestTables().forEach((table: CapturedTableProps) => {
|
|
1195
|
+
/*
|
|
1196
|
+
* Masked label, real foreign key. The admin picks "SMS: +1 ••• ••• 4821"
|
|
1197
|
+
* and the form submits `userSmsId`, so the page can point a rule at a
|
|
1198
|
+
* device whose number it was never told.
|
|
1199
|
+
*/
|
|
1200
|
+
expect(methodOptionsFor(table)).toEqual([
|
|
1201
|
+
{ label: `Email: ${MASKED_EMAIL}`, value: EMAIL_METHOD_ID },
|
|
1202
|
+
{ label: `SMS: ${MASKED_PHONE}`, value: SMS_METHOD_ID },
|
|
1203
|
+
]);
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
test("the dropdown withholds the unverified method", async () => {
|
|
1208
|
+
await renderAdminPageWithTables();
|
|
1209
|
+
|
|
1210
|
+
const values: Array<string> = methodOptionsFor(latestTables()[0]!).map(
|
|
1211
|
+
(option: { label: string; value: string }) => {
|
|
1212
|
+
return option.value;
|
|
1213
|
+
},
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
/*
|
|
1217
|
+
* Telegram is unverified in the fixture. The self-serve pages filter
|
|
1218
|
+
* `isVerified: true` in their own queries, so offering it here would be the
|
|
1219
|
+
* two surfaces disagreeing about what is pickable - and a rule pointed at an
|
|
1220
|
+
* unverified device pages nothing at all.
|
|
1221
|
+
*/
|
|
1222
|
+
expect(values).not.toContain(TELEGRAM_METHOD_ID);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
test("a rule created from that dropdown points at the method's own column", async () => {
|
|
1226
|
+
await renderAdminPageWithTables();
|
|
1227
|
+
|
|
1228
|
+
const created: UserNotificationRule =
|
|
1229
|
+
await latestTables()[0]!.onBeforeCreate(new UserNotificationRule(), {
|
|
1230
|
+
notificationMethod: SMS_METHOD_ID,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
/*
|
|
1234
|
+
* The seven foreign keys are mutually exclusive, and writing an SMS id into
|
|
1235
|
+
* `userEmailId` would page an address instead of a phone without erroring
|
|
1236
|
+
* anywhere, so the whole set is asserted rather than the one column.
|
|
1237
|
+
*/
|
|
1238
|
+
expect(methodColumnsSet(created)).toEqual([
|
|
1239
|
+
{ column: "userSmsId", value: SMS_METHOD_ID },
|
|
1240
|
+
]);
|
|
1241
|
+
expect(created.userId?.toString()).toBe(TARGET_USER_ID_STRING);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
test("with readiness unavailable the dropdown is empty rather than fetched", async () => {
|
|
1245
|
+
apiGetMock.mockResolvedValue(
|
|
1246
|
+
new HTTPErrorResponse(500, { message: "readiness is down" }, {}) as never,
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
await renderAdminPageWithTables();
|
|
1250
|
+
|
|
1251
|
+
/*
|
|
1252
|
+
* The failure mode this seam had to be designed around. Readiness is the
|
|
1253
|
+
* only source of methods here, so when it is down there are none to offer -
|
|
1254
|
+
* and "then read the models directly" is not a fallback, it is seven refused
|
|
1255
|
+
* requests that would replace the rule tables with an error page. The rules
|
|
1256
|
+
* stay editable; only the method choice is missing.
|
|
1257
|
+
*/
|
|
1258
|
+
for (const methodModel of NOTIFICATION_METHOD_MODELS) {
|
|
1259
|
+
expect(requestedModelTypes()).not.toContain(methodModel);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
latestTables().forEach((table: CapturedTableProps) => {
|
|
1263
|
+
expect(methodOptionsFor(table)).toEqual([]);
|
|
1264
|
+
expect(table.isCreateable).toBe(true);
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
test("keeps the severity id in every preferences key and namespaces the admin surface away from user settings", async () => {
|
|
1269
|
+
await renderAdminPageWithTables();
|
|
1270
|
+
|
|
1271
|
+
const adminKeys: Array<string> = capturedTables.map(
|
|
1272
|
+
(table: CapturedTableProps) => {
|
|
1273
|
+
return table.userPreferencesKey;
|
|
1274
|
+
},
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
for (const testCase of RULE_TYPE_CASES) {
|
|
1278
|
+
const keys: Array<string> = tablesFor(testCase.ruleType).map(
|
|
1279
|
+
(table: CapturedTableProps) => {
|
|
1280
|
+
return table.userPreferencesKey;
|
|
1281
|
+
},
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
testCase.severitySpecs.forEach((spec: SeveritySpec, index: number) => {
|
|
1285
|
+
expect(keys[index]).toBe(
|
|
1286
|
+
`${ADMIN_PREFERENCES_PREFIX}-${testCase.ruleType}-${spec.id}`,
|
|
1287
|
+
);
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/*
|
|
1292
|
+
* Eight tables on one route. Two sharing a key would share a stored page
|
|
1293
|
+
* size and a slice of the URL state, so paging one would repaginate the
|
|
1294
|
+
* other - invisible to the type checker, and the reason the id is in there.
|
|
1295
|
+
*/
|
|
1296
|
+
expect(new Set(adminKeys).size).toBe(8);
|
|
1297
|
+
|
|
1298
|
+
cleanup();
|
|
1299
|
+
|
|
1300
|
+
const selfServeKeys: Array<string> = [];
|
|
1301
|
+
|
|
1302
|
+
for (const testCase of RULE_TYPE_CASES) {
|
|
1303
|
+
capturedTables = [];
|
|
1304
|
+
await renderSelfServePage(testCase.Page);
|
|
1305
|
+
|
|
1306
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1307
|
+
selfServeKeys.push(table.userPreferencesKey);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
cleanup();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/*
|
|
1314
|
+
* And the admin surface has to be namespaced away from the self-serve one,
|
|
1315
|
+
* because both render the same component against the same rule types: a
|
|
1316
|
+
* shared prefix would make paging your own Sev 1 table repaginate the admin
|
|
1317
|
+
* table you last had open for a colleague.
|
|
1318
|
+
*/
|
|
1319
|
+
expect(new Set([...adminKeys, ...selfServeKeys]).size).toBe(16);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
test("never mounts a table over a notification method model", async () => {
|
|
1323
|
+
await renderAdminPageWithTables();
|
|
1324
|
+
|
|
1325
|
+
/*
|
|
1326
|
+
* The structural form of "an admin may not add a method for somebody else".
|
|
1327
|
+
* A CRUD table over UserEmail or UserSMS is how that capability would
|
|
1328
|
+
* actually arrive - not as a sentence somebody deleted, but as one more
|
|
1329
|
+
* ModelTable next to the four that are meant to be here.
|
|
1330
|
+
*/
|
|
1331
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1332
|
+
expect(table.modelType).toBe(UserNotificationRule);
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
describe("the self-serve settings pages are unchanged by the extraction", () => {
|
|
1338
|
+
for (const testCase of RULE_TYPE_CASES) {
|
|
1339
|
+
describe(testCase.label, () => {
|
|
1340
|
+
test("enumerates its own severity model and never the other one", async () => {
|
|
1341
|
+
await renderSelfServePage(testCase.Page);
|
|
1342
|
+
|
|
1343
|
+
const requestedModelTypes: Array<unknown> = getListMock.mock.calls.map(
|
|
1344
|
+
(call: Array<any>) => {
|
|
1345
|
+
return call[0].modelType;
|
|
1346
|
+
},
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
expect(requestedModelTypes).toContain(testCase.severityModelType);
|
|
1350
|
+
expect(requestedModelTypes).not.toContain(
|
|
1351
|
+
testCase.wrongSeverityModelType,
|
|
1352
|
+
);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
test("filters, creates and stores preferences exactly as it did before", async () => {
|
|
1356
|
+
await renderSelfServePage(testCase.Page);
|
|
1357
|
+
|
|
1358
|
+
getCapturedTables().forEach(
|
|
1359
|
+
(table: CapturedTableProps, index: number): void => {
|
|
1360
|
+
const spec: SeveritySpec = testCase.severitySpecs[index]!;
|
|
1361
|
+
|
|
1362
|
+
expect(table.query["ruleType"]).toBe(testCase.ruleType);
|
|
1363
|
+
expect(table.query["projectId"]?.toString()).toBe(
|
|
1364
|
+
PROJECT_ID_STRING,
|
|
1365
|
+
);
|
|
1366
|
+
expect(table.query["userId"]?.toString()).toBe(
|
|
1367
|
+
SIGNED_IN_USER_ID_STRING,
|
|
1368
|
+
);
|
|
1369
|
+
expect(table.query[testCase.foreignKeyColumn]?.toString()).toBe(
|
|
1370
|
+
spec.id,
|
|
1371
|
+
);
|
|
1372
|
+
expect(Object.keys(table.query)).not.toContain(
|
|
1373
|
+
testCase.wrongForeignKeyColumn,
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
// The prefix the deleted pages used, character for character.
|
|
1377
|
+
expect(table.userPreferencesKey).toBe(
|
|
1378
|
+
`${SELF_SERVE_PREFERENCES_PREFIX}-${testCase.ruleType}-${spec.id}`,
|
|
1379
|
+
);
|
|
1380
|
+
},
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
const created: UserNotificationRule =
|
|
1384
|
+
await getCapturedTables()[0]!.onBeforeCreate(
|
|
1385
|
+
new UserNotificationRule(),
|
|
1386
|
+
{
|
|
1387
|
+
notificationMethod: undefined,
|
|
1388
|
+
},
|
|
1389
|
+
);
|
|
1390
|
+
|
|
1391
|
+
expect(created.ruleType).toBe(testCase.ruleType);
|
|
1392
|
+
expect(created.userId?.toString()).toBe(SIGNED_IN_USER_ID_STRING);
|
|
1393
|
+
expect(created.projectId?.toString()).toBe(PROJECT_ID_STRING);
|
|
1394
|
+
expect(created[testCase.foreignKeyColumn]?.toString()).toBe(
|
|
1395
|
+
testCase.severitySpecs[0]!.id,
|
|
1396
|
+
);
|
|
1397
|
+
expect(created[testCase.wrongForeignKeyColumn]).toBeUndefined();
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("renders the card copy and table contract it had before", async () => {
|
|
1401
|
+
await renderSelfServePage(testCase.Page);
|
|
1402
|
+
|
|
1403
|
+
getCapturedTables().forEach(
|
|
1404
|
+
(table: CapturedTableProps, index: number): void => {
|
|
1405
|
+
const spec: SeveritySpec = testCase.severitySpecs[index]!;
|
|
1406
|
+
|
|
1407
|
+
expect(table.cardProps.title).toBe(testCase.getTitle(spec.name));
|
|
1408
|
+
expect(table.cardProps.description).toBe(
|
|
1409
|
+
testCase.getDescription(spec.name),
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
/*
|
|
1413
|
+
* The rest of the contract the copies carried, plus the one thing
|
|
1414
|
+
* that deliberately changed: `isEditable` is now on. It was
|
|
1415
|
+
* hardcoded false, which made the row editor unreachable from every
|
|
1416
|
+
* one of these surfaces - so opening `notifyAfterMinutes` to update
|
|
1417
|
+
* bought nothing anybody could click, and changing a delay meant
|
|
1418
|
+
* deleting the rule and adding it back, a window during which the
|
|
1419
|
+
* user is not paged at all. The edit form is not "empty" as the
|
|
1420
|
+
* previous note assumed: it carries exactly the columns the model's
|
|
1421
|
+
* update access control accepts.
|
|
1422
|
+
*/
|
|
1423
|
+
expect(table.isCreateable).toBe(true);
|
|
1424
|
+
/*
|
|
1425
|
+
* DELIBERATELY false. The built-in delete is switched off on these
|
|
1426
|
+
* tables because deleting a notification rule now goes through the
|
|
1427
|
+
* impact confirmation, which names how many rules disappear and
|
|
1428
|
+
* which severity cells lose their last delivering rule. ModelTable's
|
|
1429
|
+
* own dialog cannot carry that, so the guard supplies its own action
|
|
1430
|
+
* button instead and this flag stays off.
|
|
1431
|
+
*
|
|
1432
|
+
* If this ever reads true again, the stock "are you sure" is back and
|
|
1433
|
+
* a responder can delete their last rule for a severity without being
|
|
1434
|
+
* told - which is the failure the guard exists to prevent.
|
|
1435
|
+
*/
|
|
1436
|
+
expect(table.isDeleteable).toBe(false);
|
|
1437
|
+
expect(table.isEditable).toBe(true);
|
|
1438
|
+
expect(table.createVerb).toBe("Add");
|
|
1439
|
+
expect(table.sortBy).toBe("notifyAfterMinutes");
|
|
1440
|
+
expect(table.sortOrder).toBe(SortOrder.Ascending);
|
|
1441
|
+
expect(table.id).toBe("notification-rules");
|
|
1442
|
+
expect(table.name).toBe(
|
|
1443
|
+
`User Settings > Notification Rules > ${spec.name}`,
|
|
1444
|
+
);
|
|
1445
|
+
expect(table.noItemsMessage).toBe(NO_ITEMS_MESSAGE);
|
|
1446
|
+
|
|
1447
|
+
/*
|
|
1448
|
+
* And the form copy stays in the first person here, because on
|
|
1449
|
+
* these four pages it is true. `singularName` is left unset so the
|
|
1450
|
+
* modal, the create button and the delete confirmation keep saying
|
|
1451
|
+
* "Notification Rule" exactly as they always have.
|
|
1452
|
+
*/
|
|
1453
|
+
expect(table.singularName).toBeUndefined();
|
|
1454
|
+
expect(formFieldFor(table, "notifyAfterMinutes").title).toBe(
|
|
1455
|
+
"Notify me after",
|
|
1456
|
+
);
|
|
1457
|
+
expect(formFieldFor(table, "notificationMethod").description).toBe(
|
|
1458
|
+
"How do you want to be notified?",
|
|
1459
|
+
);
|
|
1460
|
+
},
|
|
1461
|
+
);
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
describe("the on-behalf-of banner", () => {
|
|
1468
|
+
test("names the person being edited and says the change is recorded and disclosed", async () => {
|
|
1469
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
1470
|
+
|
|
1471
|
+
expect(container.textContent).toContain(
|
|
1472
|
+
`You are editing on behalf of ${TARGET_USER_NAME}`,
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
/*
|
|
1476
|
+
* Said before the click, not after. The promise itself is kept by the
|
|
1477
|
+
* server - UserNotificationRuleService notifies the owner of every write
|
|
1478
|
+
* made by anybody else, off the server-resolved actor - but an admin should
|
|
1479
|
+
* know that their colleague is about to get an email before they start.
|
|
1480
|
+
*/
|
|
1481
|
+
expect(container.textContent).toContain("recorded in the audit log");
|
|
1482
|
+
expect(container.textContent).toContain(
|
|
1483
|
+
`${TARGET_USER_FIRST_NAME} is notified of it`,
|
|
1484
|
+
);
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
test("a viewer who may read but not edit is told so, and gets no add or remove controls", async () => {
|
|
1488
|
+
jest
|
|
1489
|
+
.spyOn(PermissionUtil, "getAllPermissions")
|
|
1490
|
+
.mockReturnValue([Permission.ReadProjectUserNotificationRule]);
|
|
1491
|
+
|
|
1492
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
1493
|
+
|
|
1494
|
+
expect(container.textContent).toContain(
|
|
1495
|
+
`You are viewing ${TARGET_USER_NAME}`,
|
|
1496
|
+
);
|
|
1497
|
+
expect(container.textContent).toContain("Edit User Notification Rules");
|
|
1498
|
+
expect(container.textContent).not.toContain("editing on behalf of");
|
|
1499
|
+
|
|
1500
|
+
/*
|
|
1501
|
+
* A convenience over the server's own check, never a substitute: the API
|
|
1502
|
+
* would refuse these writes anyway, so the page declines to draw buttons
|
|
1503
|
+
* that exist only to be rejected. All three affordances move together,
|
|
1504
|
+
* because they are one question - may this person rewrite how somebody
|
|
1505
|
+
* else gets paged? - and the server answers it once.
|
|
1506
|
+
*/
|
|
1507
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1508
|
+
expect(table.isCreateable).toBe(false);
|
|
1509
|
+
expect(table.isDeleteable).toBe(false);
|
|
1510
|
+
expect(table.isEditable).toBe(false);
|
|
1511
|
+
});
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test("a project owner keeps the controls even without the granular permission", async () => {
|
|
1515
|
+
/*
|
|
1516
|
+
* Existing teams are seeded with ROLES, never with individual granular
|
|
1517
|
+
* permissions, so a permission introduced in this release is held by nobody
|
|
1518
|
+
* until an administrator grants it. A page that checked only
|
|
1519
|
+
* EditProjectUserNotificationRule would be dead on arrival for every project
|
|
1520
|
+
* that already exists - its owner would open this page and be refused.
|
|
1521
|
+
*/
|
|
1522
|
+
jest
|
|
1523
|
+
.spyOn(PermissionUtil, "getAllPermissions")
|
|
1524
|
+
.mockReturnValue([Permission.ProjectOwner]);
|
|
1525
|
+
|
|
1526
|
+
await renderAdminPageWithTables();
|
|
1527
|
+
|
|
1528
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1529
|
+
expect(table.isCreateable).toBe(true);
|
|
1530
|
+
expect(table.isDeleteable).toBe(true);
|
|
1531
|
+
});
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
test("reading your own page never claims you are acting on somebody's behalf", async () => {
|
|
1535
|
+
jest
|
|
1536
|
+
.spyOn(UserUtil, "getUserId")
|
|
1537
|
+
.mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
|
|
1538
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
1539
|
+
|
|
1540
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
1541
|
+
|
|
1542
|
+
expect(container.textContent).toContain(
|
|
1543
|
+
"These are your own notification rules.",
|
|
1544
|
+
);
|
|
1545
|
+
expect(container.textContent).not.toContain("on behalf of");
|
|
1546
|
+
|
|
1547
|
+
/*
|
|
1548
|
+
* And a member with no admin permission at all still edits their own rules
|
|
1549
|
+
* here, exactly as they would in User Settings - the ownership branch of
|
|
1550
|
+
* the permission check, which is the one every ordinary member walks.
|
|
1551
|
+
*/
|
|
1552
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1553
|
+
expect(table.isCreateable).toBe(true);
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
test("a member with no permission is refused, and the page reads nothing about the user", async () => {
|
|
1558
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
1559
|
+
|
|
1560
|
+
window.history.pushState(
|
|
1561
|
+
{},
|
|
1562
|
+
"",
|
|
1563
|
+
`/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/notification-rules`,
|
|
1564
|
+
);
|
|
1565
|
+
|
|
1566
|
+
render(<UserViewNotificationRules {...pageProps} />);
|
|
1567
|
+
|
|
1568
|
+
expect(
|
|
1569
|
+
await screen.findByText(/do not have permission to view this user/),
|
|
1570
|
+
).toBeInTheDocument();
|
|
1571
|
+
|
|
1572
|
+
/*
|
|
1573
|
+
* Not merely "no tables drawn": no request issued either. A refused page
|
|
1574
|
+
* that still fetches a colleague's readiness has already disclosed the
|
|
1575
|
+
* thing it declined to render.
|
|
1576
|
+
*/
|
|
1577
|
+
expect(capturedTables).toHaveLength(0);
|
|
1578
|
+
expect(apiGetMock).not.toHaveBeenCalled();
|
|
1579
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
test("survives a readiness outage with the repair still on offer", async () => {
|
|
1583
|
+
apiGetMock.mockResolvedValue(
|
|
1584
|
+
new HTTPErrorResponse(500, { message: "readiness is down" }, {}) as never,
|
|
1585
|
+
);
|
|
1586
|
+
|
|
1587
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
1588
|
+
|
|
1589
|
+
expect(container.textContent).toContain("readiness is down");
|
|
1590
|
+
expect(container.textContent).toContain(
|
|
1591
|
+
`You are editing on behalf of ${TARGET_USER_NAME}`,
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
/*
|
|
1595
|
+
* Readiness is a computed opinion; the rules underneath remain perfectly
|
|
1596
|
+
* editable without it. Folding the two into one state would let a readiness
|
|
1597
|
+
* outage take away the repair this page exists to offer.
|
|
1598
|
+
*/
|
|
1599
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1600
|
+
expect(table.isCreateable).toBe(true);
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
/*
|
|
1604
|
+
* And it must not invent an empty method list out of a failed request. "No
|
|
1605
|
+
* methods" is the most alarming thing this page can print about somebody,
|
|
1606
|
+
* and printing it because a fetch failed sends an admin chasing an account
|
|
1607
|
+
* that is perfectly well configured.
|
|
1608
|
+
*/
|
|
1609
|
+
expect(container.textContent).toContain(
|
|
1610
|
+
"Notification methods could not be loaded",
|
|
1611
|
+
);
|
|
1612
|
+
expect(container.textContent).not.toContain(
|
|
1613
|
+
"has no notification methods at all",
|
|
1614
|
+
);
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
describe("notification methods are read-only", () => {
|
|
1619
|
+
test("lists each method masked, with its verification state", async () => {
|
|
1620
|
+
await renderAdminPageWithTables();
|
|
1621
|
+
|
|
1622
|
+
const methods: HTMLElement = cardNamed("Notification methods");
|
|
1623
|
+
|
|
1624
|
+
expect(methods.textContent).toContain(MASKED_EMAIL);
|
|
1625
|
+
expect(methods.textContent).toContain(MASKED_PHONE);
|
|
1626
|
+
expect(methods.textContent).toContain(MASKED_TELEGRAM);
|
|
1627
|
+
expect(methods.textContent).toContain("Verified");
|
|
1628
|
+
expect(methods.textContent).toContain("Unverified");
|
|
1629
|
+
|
|
1630
|
+
// The asymmetry stated out loud, where somebody looking for a button is.
|
|
1631
|
+
expect(methods.textContent).toContain(
|
|
1632
|
+
"Identifiers are masked and cannot be edited from here",
|
|
1633
|
+
);
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
test("offers no control for adding a method on the user's behalf", async () => {
|
|
1637
|
+
await renderAdminPageWithTables();
|
|
1638
|
+
|
|
1639
|
+
const methods: HTMLElement = cardNamed("Notification methods");
|
|
1640
|
+
|
|
1641
|
+
/*
|
|
1642
|
+
* Structural rather than copy-deep, because the next person to be tempted
|
|
1643
|
+
* will add a button, not a sentence. Letting an admin type in a phone
|
|
1644
|
+
* number would point another person's pages at a device the admin controls,
|
|
1645
|
+
* and it would not solve the case it appears to solve: a responder with no
|
|
1646
|
+
* methods needs a device THEY hold and can verify.
|
|
1647
|
+
*/
|
|
1648
|
+
expect(
|
|
1649
|
+
methods.querySelectorAll("button, input, select, textarea, form"),
|
|
1650
|
+
).toHaveLength(0);
|
|
1651
|
+
|
|
1652
|
+
/*
|
|
1653
|
+
* The only link out of this card is the mail draft. A link INTO somebody
|
|
1654
|
+
* else's notification-method settings would be the same capability wearing
|
|
1655
|
+
* a different hat.
|
|
1656
|
+
*/
|
|
1657
|
+
Array.from(methods.querySelectorAll("a")).forEach((anchor: HTMLElement) => {
|
|
1658
|
+
expect(anchor.getAttribute("href") || "").toMatch(/^mailto:/);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
test("the no-methods empty state offers a reminder rather than a form", async () => {
|
|
1663
|
+
respondWithReadiness(NO_METHODS_READINESS);
|
|
1664
|
+
|
|
1665
|
+
await renderAdminPageWithTables();
|
|
1666
|
+
|
|
1667
|
+
const methods: HTMLElement = cardNamed("Notification methods");
|
|
1668
|
+
|
|
1669
|
+
expect(methods.textContent).toContain("has no notification methods at all");
|
|
1670
|
+
expect(methods.textContent).toContain(
|
|
1671
|
+
`Only ${TARGET_USER_FIRST_NAME} can add one`,
|
|
1672
|
+
);
|
|
1673
|
+
|
|
1674
|
+
// A nudge, with the link already in it - and still not a single control.
|
|
1675
|
+
expect(
|
|
1676
|
+
methods.querySelectorAll("button, input, select, textarea, form"),
|
|
1677
|
+
).toHaveLength(0);
|
|
1678
|
+
|
|
1679
|
+
const href: string = mailtoHrefIn(methods);
|
|
1680
|
+
|
|
1681
|
+
expect(href).toContain(TARGET_LOGIN_EMAIL);
|
|
1682
|
+
expect(href).toContain("notification-methods");
|
|
1683
|
+
expect(href).toContain(
|
|
1684
|
+
"only you can add the device or address they send to",
|
|
1685
|
+
);
|
|
1686
|
+
|
|
1687
|
+
/*
|
|
1688
|
+
* The draft has to say what is actually wrong, or it is a form letter. With
|
|
1689
|
+
* no method at all the consequence is not a late page, it is no page.
|
|
1690
|
+
*/
|
|
1691
|
+
expect(href).toContain("dropped");
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("the rule tables stay editable while the methods stay locked", async () => {
|
|
1695
|
+
respondWithReadiness(NO_METHODS_READINESS);
|
|
1696
|
+
|
|
1697
|
+
await renderAdminPageWithTables();
|
|
1698
|
+
|
|
1699
|
+
/*
|
|
1700
|
+
* The whole design of this phase in one assertion: rules are the admin's to
|
|
1701
|
+
* repair, methods are not. A responder with no verified method still needs
|
|
1702
|
+
* rules waiting for the moment they add one.
|
|
1703
|
+
*/
|
|
1704
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1705
|
+
expect(table.isCreateable).toBe(true);
|
|
1706
|
+
expect(table.isDeleteable).toBe(true);
|
|
1707
|
+
expect(table.modelType).toBe(UserNotificationRule);
|
|
1708
|
+
});
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
/*
|
|
1713
|
+
* The coverage grid is shared code now.
|
|
1714
|
+
*
|
|
1715
|
+
* CoverageMatrix, CoverageModel and buildCoverageModel started module-private
|
|
1716
|
+
* inside Pages/OnCallDuty/Readiness.tsx; this page imported them from a
|
|
1717
|
+
* Components path that did not exist, so it could not compile at all. They live
|
|
1718
|
+
* in Components/OnCallPolicy/Readiness/CoverageMatrix now and both surfaces
|
|
1719
|
+
* import them from there - which is the only arrangement in which "see the gap
|
|
1720
|
+
* on the readiness page, fix it here" cannot become two different opinions about
|
|
1721
|
+
* what a gap is.
|
|
1722
|
+
*
|
|
1723
|
+
* The assertions below are about the property that mattered enough to build the
|
|
1724
|
+
* model around it: incident severities and alert severities are DISJOINT id
|
|
1725
|
+
* sets, so one shared axis draws cells that can never exist and a reader cannot
|
|
1726
|
+
* tell an impossible cell from a real hole.
|
|
1727
|
+
*/
|
|
1728
|
+
describe("the coverage grid, now shared with the readiness page", () => {
|
|
1729
|
+
test("draws one matrix per severity kind", async () => {
|
|
1730
|
+
await renderAdminPageWithTables();
|
|
1731
|
+
|
|
1732
|
+
expect(matrixFor("Incident severities")).toBeInTheDocument();
|
|
1733
|
+
expect(matrixFor("Alert severities")).toBeInTheDocument();
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
test("neither kind's severities appear under the other kind's columns", async () => {
|
|
1737
|
+
await renderAdminPageWithTables();
|
|
1738
|
+
|
|
1739
|
+
const incidentMatrix: HTMLElement = matrixFor("Incident severities");
|
|
1740
|
+
const alertMatrix: HTMLElement = matrixFor("Alert severities");
|
|
1741
|
+
|
|
1742
|
+
expect(
|
|
1743
|
+
within(incidentMatrix).getByText(INCIDENT_SEVERITY_ONE_NAME),
|
|
1744
|
+
).toBeInTheDocument();
|
|
1745
|
+
expect(
|
|
1746
|
+
within(incidentMatrix).queryByText(ALERT_SEVERITY_ONE_NAME),
|
|
1747
|
+
).toBeNull();
|
|
1748
|
+
|
|
1749
|
+
expect(
|
|
1750
|
+
within(alertMatrix).getByText(ALERT_SEVERITY_ONE_NAME),
|
|
1751
|
+
).toBeInTheDocument();
|
|
1752
|
+
expect(
|
|
1753
|
+
within(alertMatrix).queryByText(INCIDENT_SEVERITY_ONE_NAME),
|
|
1754
|
+
).toBeNull();
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
test("marks the severity with no rule as a gap rather than as covered", async () => {
|
|
1758
|
+
await renderAdminPageWithTables();
|
|
1759
|
+
|
|
1760
|
+
const incidentMatrix: HTMLElement = matrixFor("Incident severities");
|
|
1761
|
+
|
|
1762
|
+
/*
|
|
1763
|
+
* The fixture gives Sev One a rule and Sev Two none, so exactly one cell in
|
|
1764
|
+
* this matrix is a hole. Counting by the cell's own title is what makes the
|
|
1765
|
+
* assertion about MEANING rather than about a class name.
|
|
1766
|
+
*/
|
|
1767
|
+
expect(
|
|
1768
|
+
incidentMatrix.querySelectorAll('span[title^="No rule"]'),
|
|
1769
|
+
).toHaveLength(1);
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
test("a severity-less rule type is listed apart from the grids", async () => {
|
|
1773
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
1774
|
+
|
|
1775
|
+
/*
|
|
1776
|
+
* WHEN_USER_GOES_ON_CALL carries no severity at all. Putting it in a
|
|
1777
|
+
* severity grid would need a row for a severity it does not have, so it
|
|
1778
|
+
* belongs under its own heading.
|
|
1779
|
+
*/
|
|
1780
|
+
expect(container.textContent).toContain("Shift changes");
|
|
1781
|
+
});
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
/*
|
|
1785
|
+
* The edit path.
|
|
1786
|
+
*
|
|
1787
|
+
* `isEditable` was hardcoded false on the ModelTable, so this page issued no
|
|
1788
|
+
* PATCH at all: the phase opened `notifyAfterMinutes` (and the seven method
|
|
1789
|
+
* foreign keys) to update behind the largest guard in the codebase, and bought
|
|
1790
|
+
* zero capability anybody could reach. These assertions are about the two halves
|
|
1791
|
+
* of making that real - the editor being on for exactly the viewers allowed to
|
|
1792
|
+
* use it, and the form offering exactly the columns the server will actually
|
|
1793
|
+
* accept on the update path.
|
|
1794
|
+
*/
|
|
1795
|
+
describe("the rules are editable, not merely addable and removable", () => {
|
|
1796
|
+
test("an admin gets the row editor on every table", async () => {
|
|
1797
|
+
await renderAdminPageWithTables();
|
|
1798
|
+
|
|
1799
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1800
|
+
expect(table.isEditable).toBe(true);
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
test("a member editing their own rules gets it too", async () => {
|
|
1805
|
+
jest
|
|
1806
|
+
.spyOn(UserUtil, "getUserId")
|
|
1807
|
+
.mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
|
|
1808
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
1809
|
+
|
|
1810
|
+
await renderAdminPageWithTables();
|
|
1811
|
+
|
|
1812
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1813
|
+
expect(table.isEditable).toBe(true);
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
test("the form offers the delay, which is what the server lets an editor change", async () => {
|
|
1818
|
+
await renderAdminPageWithTables();
|
|
1819
|
+
|
|
1820
|
+
const table: CapturedTableProps = capturedTables[0]!;
|
|
1821
|
+
const delayField: CapturedFormField = formFieldFor(
|
|
1822
|
+
table,
|
|
1823
|
+
"notifyAfterMinutes",
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
/*
|
|
1827
|
+
* `notifyAfterMinutes` is `update: [CurrentUser, ...admin]` on the model -
|
|
1828
|
+
* the one plain preference an editor may rewrite - so it has to survive
|
|
1829
|
+
* into the edit form. A field marked doNotShowWhenEditing here would leave
|
|
1830
|
+
* the editor with nothing to change and the PATCH with no columns, which
|
|
1831
|
+
* the API rejects outright.
|
|
1832
|
+
*/
|
|
1833
|
+
expect(delayField.doNotShowWhenEditing).toBeFalsy();
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
test("the form withholds the method dropdown when editing, because update cannot carry it", async () => {
|
|
1837
|
+
await renderAdminPageWithTables();
|
|
1838
|
+
|
|
1839
|
+
const methodField: CapturedFormField = formFieldFor(
|
|
1840
|
+
capturedTables[0]!,
|
|
1841
|
+
"notificationMethod",
|
|
1842
|
+
);
|
|
1843
|
+
|
|
1844
|
+
/*
|
|
1845
|
+
* The dropdown is an override field: its value travels in `miscDataProps`
|
|
1846
|
+
* and is mapped onto one of the seven foreign keys by `onBeforeCreate`.
|
|
1847
|
+
* ModelForm calls that hook only for FormType.Create and BaseAPI.updateItem
|
|
1848
|
+
* never reads `miscDataProps`, so on the edit path the choice is discarded
|
|
1849
|
+
* in transit. Rendering it anyway would give an administrator a control
|
|
1850
|
+
* that accepts a new device, reports success, and leaves the rule paging
|
|
1851
|
+
* the old one - a worse outcome than not offering it.
|
|
1852
|
+
*/
|
|
1853
|
+
expect(methodField.doNotShowWhenEditing).toBe(true);
|
|
1854
|
+
expect(methodField.overrideFieldKey).toBe("notificationMethod");
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
test("the form offers nothing the model refuses to update", async () => {
|
|
1858
|
+
await renderAdminPageWithTables();
|
|
1859
|
+
|
|
1860
|
+
const keys: Array<string> = formFieldKeys(capturedTables[0]!);
|
|
1861
|
+
|
|
1862
|
+
/*
|
|
1863
|
+
* Every one of these is `update: []` on UserNotificationRule: they are the
|
|
1864
|
+
* rule's ADDRESS - whose it is, what fires it, at which severity - and a
|
|
1865
|
+
* form that offered them would be offering to move somebody else's rule
|
|
1866
|
+
* onto a different person or a different severity band. The API would
|
|
1867
|
+
* refuse; the form does not ask.
|
|
1868
|
+
*/
|
|
1869
|
+
for (const frozenColumn of [
|
|
1870
|
+
"userId",
|
|
1871
|
+
"user",
|
|
1872
|
+
"projectId",
|
|
1873
|
+
"ruleType",
|
|
1874
|
+
"incidentSeverityId",
|
|
1875
|
+
"alertSeverityId",
|
|
1876
|
+
]) {
|
|
1877
|
+
expect(keys).not.toContain(frozenColumn);
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
/*
|
|
1883
|
+
* Three ways this page told the truth on one screen and not on another.
|
|
1884
|
+
*/
|
|
1885
|
+
describe("the page says the same thing at the moment of the write", () => {
|
|
1886
|
+
test("the modal, the create button and the delete prompt all name the person", async () => {
|
|
1887
|
+
await renderAdminPageWithTables();
|
|
1888
|
+
|
|
1889
|
+
/*
|
|
1890
|
+
* ModelTable derives the modal title, the create button and the delete
|
|
1891
|
+
* confirmation from `singularName`, so setting it once puts the name on all
|
|
1892
|
+
* three. This is the fix for the banner problem: the on-behalf-of strip is
|
|
1893
|
+
* sticky but a modal covers it, and the modal is open at exactly the moment
|
|
1894
|
+
* an administrator is about to rewrite how a colleague gets paged.
|
|
1895
|
+
*/
|
|
1896
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1897
|
+
expect(table.singularName).toBe(
|
|
1898
|
+
`Notification Rule for ${TARGET_USER_NAME}`,
|
|
1899
|
+
);
|
|
1900
|
+
});
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
test("the form fields stop speaking in the first person", async () => {
|
|
1904
|
+
await renderAdminPageWithTables();
|
|
1905
|
+
|
|
1906
|
+
const table: CapturedTableProps = capturedTables[0]!;
|
|
1907
|
+
|
|
1908
|
+
expect(formFieldFor(table, "notifyAfterMinutes").title).toBe(
|
|
1909
|
+
`Notify ${TARGET_USER_NAME} after`,
|
|
1910
|
+
);
|
|
1911
|
+
expect(formFieldFor(table, "notificationMethod").description).toBe(
|
|
1912
|
+
`How should ${TARGET_USER_NAME} be notified?`,
|
|
1913
|
+
);
|
|
1914
|
+
|
|
1915
|
+
/*
|
|
1916
|
+
* "Notify me after" inside a modal opened from somebody else's page reads
|
|
1917
|
+
* as the admin's own configuration, which is the single mistake this whole
|
|
1918
|
+
* surface has to prevent.
|
|
1919
|
+
*/
|
|
1920
|
+
expect(formFieldFor(table, "notifyAfterMinutes").title).not.toContain(
|
|
1921
|
+
" me ",
|
|
1922
|
+
);
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
test("reading your own page keeps the first person", async () => {
|
|
1926
|
+
jest
|
|
1927
|
+
.spyOn(UserUtil, "getUserId")
|
|
1928
|
+
.mockReturnValue(new ObjectID(TARGET_USER_ID_STRING));
|
|
1929
|
+
|
|
1930
|
+
await renderAdminPageWithTables();
|
|
1931
|
+
|
|
1932
|
+
const table: CapturedTableProps = capturedTables[0]!;
|
|
1933
|
+
|
|
1934
|
+
expect(table.singularName).toBeUndefined();
|
|
1935
|
+
expect(formFieldFor(table, "notifyAfterMinutes").title).toBe(
|
|
1936
|
+
"Notify me after",
|
|
1937
|
+
);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
test("an opt-out row is labelled rather than left blank", async () => {
|
|
1941
|
+
await renderAdminPageWithTables();
|
|
1942
|
+
|
|
1943
|
+
const methodColumn: CapturedColumn = columnNamed(
|
|
1944
|
+
capturedTables[0]!,
|
|
1945
|
+
"Notification Method",
|
|
1946
|
+
);
|
|
1947
|
+
|
|
1948
|
+
const optOutRule: UserNotificationRule = new UserNotificationRule();
|
|
1949
|
+
optOutRule.isOptOut = true;
|
|
1950
|
+
optOutRule.notifyAfterMinutes = 0;
|
|
1951
|
+
|
|
1952
|
+
const { container } = render(methodColumn.getElement!(optOutRule));
|
|
1953
|
+
|
|
1954
|
+
/*
|
|
1955
|
+
* An opt-out rule carries no notification method by design, so the method
|
|
1956
|
+
* cell renders empty - indistinguishable from a rule whose relation failed
|
|
1957
|
+
* to load, and from a corrupt row. An administrator who reads a blank as
|
|
1958
|
+
* broken deletes it, which silently turns paging back on for somebody who
|
|
1959
|
+
* asked for silence.
|
|
1960
|
+
*/
|
|
1961
|
+
expect(container.textContent).toContain("Muted");
|
|
1962
|
+
expect(container.textContent).not.toBe("");
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
test("an ordinary rule is not labelled as muted", async () => {
|
|
1966
|
+
await renderAdminPageWithTables();
|
|
1967
|
+
|
|
1968
|
+
const methodColumn: CapturedColumn = columnNamed(
|
|
1969
|
+
capturedTables[0]!,
|
|
1970
|
+
"Notification Method",
|
|
1971
|
+
);
|
|
1972
|
+
|
|
1973
|
+
const plainRule: UserNotificationRule = new UserNotificationRule();
|
|
1974
|
+
plainRule.notifyAfterMinutes = 5;
|
|
1975
|
+
|
|
1976
|
+
const { container } = render(methodColumn.getElement!(plainRule));
|
|
1977
|
+
|
|
1978
|
+
expect(container.textContent).not.toContain("Muted");
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
test("the opt-out row is selected for, or the label could never fire", async () => {
|
|
1982
|
+
await renderAdminPageWithTables();
|
|
1983
|
+
|
|
1984
|
+
/*
|
|
1985
|
+
* The cell can only branch on a column the table actually asked the API
|
|
1986
|
+
* for. Without this the flag arrives undefined on every row and every
|
|
1987
|
+
* opt-out renders blank again - the defect restored, with a passing-looking
|
|
1988
|
+
* component.
|
|
1989
|
+
*/
|
|
1990
|
+
capturedTables.forEach((table: CapturedTableProps) => {
|
|
1991
|
+
expect(table.selectMoreFields["isOptOut"]).toBe(true);
|
|
1992
|
+
});
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
test("the empty state promises a fallback only where the project has one", async () => {
|
|
1996
|
+
await renderAdminPageWithTables();
|
|
1997
|
+
|
|
1998
|
+
latestTables().forEach((table: CapturedTableProps) => {
|
|
1999
|
+
expect(table.noItemsMessage).toContain(
|
|
2000
|
+
"fall back to whatever verified method they have",
|
|
2001
|
+
);
|
|
2002
|
+
});
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
test("with the fallback switched off it says the pages are dropped instead", async () => {
|
|
2006
|
+
const project: Project = new Project();
|
|
2007
|
+
project.disableOnCallNotificationFallback = true;
|
|
2008
|
+
getItemMock.mockResolvedValue(project as never);
|
|
2009
|
+
|
|
2010
|
+
await renderAdminPageWithTables();
|
|
2011
|
+
|
|
2012
|
+
/*
|
|
2013
|
+
* The same hole means two different things depending on one project
|
|
2014
|
+
* setting, and this page already prints a banner about it - so promising a
|
|
2015
|
+
* fallback in the empty state underneath was the page contradicting itself
|
|
2016
|
+
* on one screen. With the switch off a missing rule is not a late page, it
|
|
2017
|
+
* is no page.
|
|
2018
|
+
*/
|
|
2019
|
+
await waitFor((): void => {
|
|
2020
|
+
latestTables().forEach((table: CapturedTableProps) => {
|
|
2021
|
+
expect(table.noItemsMessage).toContain("dropped");
|
|
2022
|
+
expect(table.noItemsMessage).not.toContain("fall back to whatever");
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
test("when the setting cannot be read it claims neither outcome", async () => {
|
|
2028
|
+
apiGetMock.mockResolvedValue(
|
|
2029
|
+
new HTTPErrorResponse(500, { message: "readiness is down" }, {}) as never,
|
|
2030
|
+
);
|
|
2031
|
+
|
|
2032
|
+
await renderAdminPageWithTables();
|
|
2033
|
+
|
|
2034
|
+
/*
|
|
2035
|
+
* A failed read is not a project with the fallback off. Saying "your pages
|
|
2036
|
+
* are dropped" because a request timed out is a specific and possibly false
|
|
2037
|
+
* claim, and saying they fall back is the comforting version of the same
|
|
2038
|
+
* mistake.
|
|
2039
|
+
*/
|
|
2040
|
+
await waitFor((): void => {
|
|
2041
|
+
latestTables().forEach((table: CapturedTableProps) => {
|
|
2042
|
+
expect(table.noItemsMessage).toContain("could not be read");
|
|
2043
|
+
expect(table.noItemsMessage).not.toContain("dropped");
|
|
2044
|
+
expect(table.noItemsMessage).not.toContain("fall back to whatever");
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
describe("no unmasked identifier reaches the DOM", () => {
|
|
2051
|
+
type AssertNoRawIdentifiersFunction = (container: HTMLElement) => void;
|
|
2052
|
+
|
|
2053
|
+
const assertNoRawIdentifiers: AssertNoRawIdentifiersFunction = (
|
|
2054
|
+
container: HTMLElement,
|
|
2055
|
+
): void => {
|
|
2056
|
+
/*
|
|
2057
|
+
* innerHTML rather than textContent, because a leak is just as real in an
|
|
2058
|
+
* href, a title or a data attribute as it is in visible copy.
|
|
2059
|
+
*/
|
|
2060
|
+
for (const raw of ALL_RAW_IDENTIFIERS) {
|
|
2061
|
+
expect(container.innerHTML).not.toContain(raw);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/*
|
|
2065
|
+
* The scan above catches the planted values by name; these two catch the
|
|
2066
|
+
* shape, so an identifier this file never thought of is caught too.
|
|
2067
|
+
*/
|
|
2068
|
+
expect(container.textContent || "").not.toMatch(UNMASKED_EMAIL_PATTERN);
|
|
2069
|
+
expect(container.textContent || "").not.toMatch(UNMASKED_PHONE_PATTERN);
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
test("the admin page renders masked identifiers only", async () => {
|
|
2073
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
2074
|
+
|
|
2075
|
+
// The masked forms are present, so this is not passing by rendering nothing.
|
|
2076
|
+
expect(container.textContent).toContain(MASKED_EMAIL);
|
|
2077
|
+
expect(container.textContent).toContain(MASKED_PHONE);
|
|
2078
|
+
|
|
2079
|
+
assertNoRawIdentifiers(container);
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
test("an unreachable user's empty state leaks nothing either", async () => {
|
|
2083
|
+
respondWithReadiness(NO_METHODS_READINESS);
|
|
2084
|
+
|
|
2085
|
+
const container: HTMLElement = await renderAdminPageWithTables();
|
|
2086
|
+
|
|
2087
|
+
expect(container.textContent).toContain(
|
|
2088
|
+
"has no notification methods at all",
|
|
2089
|
+
);
|
|
2090
|
+
|
|
2091
|
+
assertNoRawIdentifiers(container);
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
/*
|
|
2095
|
+
* The dropdown labels are not in the DOM here - ModelTable is a stub, and the
|
|
2096
|
+
* real one renders them only once a modal is opened - so the scans above
|
|
2097
|
+
* cannot see them. They are the newest path a raw identifier could travel, so
|
|
2098
|
+
* they are checked at the prop.
|
|
2099
|
+
*/
|
|
2100
|
+
test("no dropdown label carries a raw identifier", async () => {
|
|
2101
|
+
await renderAdminPageWithTables();
|
|
2102
|
+
|
|
2103
|
+
const labels: Array<string> = [];
|
|
2104
|
+
|
|
2105
|
+
latestTables().forEach((table: CapturedTableProps) => {
|
|
2106
|
+
methodOptionsFor(table).forEach(
|
|
2107
|
+
(option: { label: string; value: string }) => {
|
|
2108
|
+
labels.push(option.label);
|
|
2109
|
+
},
|
|
2110
|
+
);
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
// Not vacuous: there are labels, and they are the masked ones.
|
|
2114
|
+
expect(labels.length).toBeGreaterThan(0);
|
|
2115
|
+
expect(labels.join(" ")).toContain(MASKED_PHONE);
|
|
2116
|
+
|
|
2117
|
+
for (const label of labels) {
|
|
2118
|
+
for (const raw of ALL_RAW_IDENTIFIERS) {
|
|
2119
|
+
expect(label).not.toContain(raw);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
expect(label).not.toMatch(UNMASKED_EMAIL_PATTERN);
|
|
2123
|
+
expect(label).not.toMatch(UNMASKED_PHONE_PATTERN);
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
/*
|
|
2128
|
+
* The mail draft is the one place a whole address is legitimate - it is the
|
|
2129
|
+
* mailto recipient, and it is the login email an admin can already read on
|
|
2130
|
+
* every other screen in the product. It must never be a notification-method
|
|
2131
|
+
* identifier.
|
|
2132
|
+
*/
|
|
2133
|
+
test("the mail draft carries the login address and no method identifier", async () => {
|
|
2134
|
+
respondWithReadiness(NO_METHODS_READINESS);
|
|
2135
|
+
|
|
2136
|
+
await renderAdminPageWithTables();
|
|
2137
|
+
|
|
2138
|
+
const href: string = mailtoHrefIn(cardNamed("Notification methods"));
|
|
2139
|
+
|
|
2140
|
+
expect(href).toContain(TARGET_LOGIN_EMAIL);
|
|
2141
|
+
|
|
2142
|
+
for (const raw of ALL_RAW_IDENTIFIERS) {
|
|
2143
|
+
expect(href).not.toContain(raw);
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
});
|