@oneuptime/common 12.0.13 → 12.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
- package/Models/DatabaseModels/UserCall.ts +94 -3
- package/Models/DatabaseModels/UserEmail.ts +94 -3
- package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
- package/Models/DatabaseModels/UserPush.ts +37 -0
- package/Models/DatabaseModels/UserSMS.ts +94 -3
- package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
- package/Server/API/BaseAPI.ts +79 -14
- package/Server/API/UserCallAPI.ts +53 -45
- package/Server/API/UserEmailAPI.ts +53 -44
- package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
- package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
- package/Server/API/UserPushAPI.ts +126 -2
- package/Server/API/UserSmsAPI.ts +53 -44
- package/Server/API/UserWhatsAppAPI.ts +53 -50
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
- package/Server/Services/AnalyticsDatabaseService.ts +169 -0
- package/Server/Services/DatabaseService.ts +101 -0
- package/Server/Services/LogAggregationService.ts +5 -0
- package/Server/Services/PushNotificationService.ts +127 -12
- package/Server/Services/TeamMemberService.ts +53 -13
- package/Server/Services/UserCallService.ts +55 -24
- package/Server/Services/UserEmailService.ts +83 -17
- package/Server/Services/UserIncomingCallNumberService.ts +49 -85
- package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
- package/Server/Services/UserNotificationRuleService.ts +26 -0
- package/Server/Services/UserPushService.ts +89 -0
- package/Server/Services/UserSmsService.ts +50 -17
- package/Server/Services/UserWhatsAppService.ts +48 -15
- package/Server/Types/Database/QueryHelper.ts +30 -0
- package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
- package/Server/Utils/ChannelVerification.ts +510 -0
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
- package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
- package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
- package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
- package/Server/Utils/TotpAuth.ts +137 -9
- package/Server/Utils/UserRegistrationToken.ts +182 -0
- package/Server/Utils/VerificationCode.ts +134 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
- package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
- package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
- package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
- package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
- package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
- package/Tests/Server/API/UserSmsApi.test.ts +107 -14
- package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
- package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
- package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
- package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
- package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
- package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
- package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
- package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
- package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
- package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
- package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
- package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
- package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
- package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
- package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
- package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
- package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
- package/Tests/Server/Utils/Browser.test.ts +162 -0
- package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
- package/Tests/Server/Utils/Express.test.ts +371 -0
- package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
- package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
- package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
- package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
- package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
- package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
- package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
- package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
- package/Tests/Types/Call/CallRequest.test.ts +189 -0
- package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
- package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
- package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
- package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
- package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
- package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
- package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
- package/Tests/Types/TextRandomGeneration.test.ts +211 -0
- package/Tests/UI/Components/Button.test.tsx +175 -1
- package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
- package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
- package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
- package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
- package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
- package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
- package/Tests/UI/Components/List.test.tsx +9 -2
- package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
- package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
- package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
- package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
- package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
- package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
- package/Tests/UI/Components/Skeleton.test.tsx +71 -0
- package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
- package/Tests/UI/Components/Tabs.test.tsx +24 -0
- package/Tests/UI/Components/Toast.test.tsx +130 -7
- package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
- package/Tests/UI/Utils/Dropdown.test.ts +423 -0
- package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
- package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
- package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
- package/Tests/Utils/API.test.ts +16 -2
- package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
- package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
- package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
- package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
- package/Types/Permission.ts +35 -0
- package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
- package/Types/PushNotification/PushNotificationMessage.ts +15 -0
- package/Types/PushNotification/PushNotificationRequest.ts +8 -16
- package/Types/Service/ServiceLanguage.ts +144 -0
- package/Types/Text.ts +111 -19
- package/UI/Components/Button/Button.tsx +27 -14
- package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
- package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
- package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
- package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
- package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
- package/UI/Components/CommandPalette/Types.ts +49 -0
- package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
- package/UI/Components/Dropdown/Dropdown.tsx +6 -1
- package/UI/Components/Feed/FeedItem.tsx +1 -1
- package/UI/Components/List/List.tsx +37 -5
- package/UI/Components/List/ListSkeleton.tsx +54 -0
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
- package/UI/Components/Modal/Modal.tsx +2 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
- package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
- package/UI/Components/Navbar/NavBar.tsx +14 -1
- package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
- package/UI/Components/SideOver/SideOver.tsx +37 -2
- package/UI/Components/Skeleton/Skeleton.tsx +53 -0
- package/UI/Components/Table/Table.tsx +74 -44
- package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
- package/UI/Components/Tabs/Tab.tsx +1 -1
- package/UI/Components/Toast/Toast.tsx +118 -53
- package/UI/Components/Toast/ToastInit.tsx +82 -32
- package/UI/Styles/Theme.css +40 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
- package/UI/Utils/ModelListCache.ts +188 -0
- package/UI/Utils/Project.ts +29 -0
- package/Utils/API.ts +6 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +69 -13
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +42 -38
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +42 -38
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
- package/build/dist/Server/API/UserPushAPI.js +95 -2
- package/build/dist/Server/API/UserPushAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +42 -38
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +74 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +5 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/PushNotificationService.js +69 -30
- package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +43 -7
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +54 -22
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +78 -17
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
- package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +77 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +50 -17
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
- package/build/dist/Server/Utils/ChannelVerification.js +348 -0
- package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/TotpAuth.js +127 -8
- package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
- package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
- package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
- package/build/dist/Server/Utils/VerificationCode.js +120 -0
- package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
- package/build/dist/Types/Permission.js +33 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
- package/build/dist/Types/Service/ServiceLanguage.js +96 -0
- package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
- package/build/dist/Types/Text.js +82 -17
- package/build/dist/Types/Text.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +22 -14
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
- package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
- package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +19 -5
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
- package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
- package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
- package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
- package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
- package/build/dist/UI/Components/Table/Table.js +35 -24
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
- package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
- package/build/dist/UI/Components/Tabs/Tab.js +1 -1
- package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
- package/build/dist/UI/Components/Toast/Toast.js +77 -23
- package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
- package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelListCache.js +126 -0
- package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
- package/build/dist/UI/Utils/Project.js +24 -1
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/Utils/API.js +6 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/jest.config.json +1 -1
- package/package.json +2 -3
- package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
- package/Tests/__mocks__/otpauth.js +0 -30
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
|
@@ -0,0 +1,905 @@
|
|
|
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 { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
11
|
+
import React, { ReactElement } from "react";
|
|
12
|
+
import {
|
|
13
|
+
Location,
|
|
14
|
+
MemoryRouter,
|
|
15
|
+
Outlet,
|
|
16
|
+
Route as RouterRoute,
|
|
17
|
+
Routes as RouterRoutes,
|
|
18
|
+
} from "react-router-dom";
|
|
19
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* THE SPLIT ITSELF.
|
|
23
|
+
*
|
|
24
|
+
* Users > View > Notification Rules was one route that rendered, in one scroll:
|
|
25
|
+
* a readiness summary with four stat tiles and a prose consequence, a masked
|
|
26
|
+
* list of the person's notification methods, a coverage grid, and four rule
|
|
27
|
+
* types that each expand to one card per severity band. On a project with six
|
|
28
|
+
* incident and six alert severities that is around fifty cards under a
|
|
29
|
+
* diagnosis nobody could still see by the time they reached the controls.
|
|
30
|
+
*
|
|
31
|
+
* It is six pages now, and the failures worth writing down about a page split
|
|
32
|
+
* are not "does the new page render" — the suites next door already assert what
|
|
33
|
+
* each page contains. They are the ones that make a split silently WORSE than
|
|
34
|
+
* what it replaced:
|
|
35
|
+
*
|
|
36
|
+
* - A DEAD BOOKMARK. The old URL is in tickets, chat scrollback and at least
|
|
37
|
+
* one runbook. It has to keep landing somewhere useful, and "somewhere
|
|
38
|
+
* useful" is the overview, not a 404 and not the middle of the rule pages.
|
|
39
|
+
*
|
|
40
|
+
* - A PAGE THAT STILL RENDERS EVERYTHING. Splitting a route four ways buys
|
|
41
|
+
* nothing if each of the four still mounts all four rule types. The
|
|
42
|
+
* assertion is per-route and counts the tables that actually mounted.
|
|
43
|
+
*
|
|
44
|
+
* - THE WRONG SEVERITY AXIS ON ONE ROUTE. A rule is tied to its band by
|
|
45
|
+
* `incidentSeverityId` or `alertSeverityId`, and the severity MODEL does
|
|
46
|
+
* not follow from the rule type the way the names suggest: an alert episode
|
|
47
|
+
* is banded by AlertSeverity, an incident episode by IncidentSeverity.
|
|
48
|
+
* Splitting four tables across four routes is exactly the edit in which one
|
|
49
|
+
* of them gets the other's props, and nothing throws — the table just lists
|
|
50
|
+
* every severity's rules at once. The two severity models here return
|
|
51
|
+
* DISJOINT id sets, which is the only fixture shape in which that is
|
|
52
|
+
* visible.
|
|
53
|
+
*
|
|
54
|
+
* - A SECTION THAT REFETCHES PER PAGE. The six pages share one layout that
|
|
55
|
+
* loads the target user and their readiness once. If a page reached for its
|
|
56
|
+
* own copy, moving between them would issue a fresh pair of reads each time
|
|
57
|
+
* and — much worse — two pages could disagree about WHO they are editing,
|
|
58
|
+
* which is the failure this whole surface is written to avoid.
|
|
59
|
+
*
|
|
60
|
+
* - AN UNREACHABLE PAGE. Six routes are worth nothing if the menu names four,
|
|
61
|
+
* so the side menu is asserted against the route table rather than against
|
|
62
|
+
* a list of strings copied out of the component.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
const PROJECT_ID_STRING: string = "10000000-0000-4000-8000-000000000001";
|
|
66
|
+
const SIGNED_IN_USER_ID_STRING: string = "20000000-0000-4000-8000-000000000002";
|
|
67
|
+
const TARGET_USER_ID_STRING: string = "30000000-0000-4000-8000-000000000003";
|
|
68
|
+
|
|
69
|
+
const INCIDENT_SEVERITY_ONE_ID: string = "41111111-1111-4111-8111-111111111111";
|
|
70
|
+
const INCIDENT_SEVERITY_TWO_ID: string = "42222222-2222-4222-8222-222222222222";
|
|
71
|
+
const ALERT_SEVERITY_ONE_ID: string = "51111111-1111-4111-8111-111111111111";
|
|
72
|
+
const ALERT_SEVERITY_TWO_ID: string = "52222222-2222-4222-8222-222222222222";
|
|
73
|
+
|
|
74
|
+
const TARGET_USER_NAME: string = "Jane Ops";
|
|
75
|
+
const TARGET_LOGIN_EMAIL: string = "jane.ops@example.com";
|
|
76
|
+
|
|
77
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
78
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
79
|
+
const getCommonHeadersMock: MockFunction = getJestMockFunction();
|
|
80
|
+
const apiGetMock: MockFunction = getJestMockFunction();
|
|
81
|
+
const navigateMock: MockFunction = getJestMockFunction();
|
|
82
|
+
|
|
83
|
+
let pendingRequestCount: number = 0;
|
|
84
|
+
|
|
85
|
+
type TrackRequestFunction = (result: unknown) => unknown;
|
|
86
|
+
|
|
87
|
+
const trackRequest: TrackRequestFunction = (result: unknown): unknown => {
|
|
88
|
+
if (!(result instanceof Promise)) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pendingRequestCount++;
|
|
93
|
+
|
|
94
|
+
return result.finally((): void => {
|
|
95
|
+
pendingRequestCount--;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
100
|
+
return {
|
|
101
|
+
__esModule: true,
|
|
102
|
+
default: {
|
|
103
|
+
getList: (...args: Array<any>) => {
|
|
104
|
+
return trackRequest(getListMock(...args));
|
|
105
|
+
},
|
|
106
|
+
getItem: (...args: Array<any>) => {
|
|
107
|
+
return trackRequest(getItemMock(...args));
|
|
108
|
+
},
|
|
109
|
+
getCommonHeaders: (...args: Array<any>) => {
|
|
110
|
+
return getCommonHeadersMock(...args);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
117
|
+
return {
|
|
118
|
+
__esModule: true,
|
|
119
|
+
default: {
|
|
120
|
+
get: (...args: Array<any>) => {
|
|
121
|
+
return trackRequest(apiGetMock(...args));
|
|
122
|
+
},
|
|
123
|
+
getFriendlyMessage: (error: unknown) => {
|
|
124
|
+
return (
|
|
125
|
+
((error as { message?: unknown } | null)?.message as string) ||
|
|
126
|
+
"Could not load"
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
getFriendlyErrorMessage: (error: unknown) => {
|
|
130
|
+
return (
|
|
131
|
+
((error as { message?: unknown } | null)?.message as string) ||
|
|
132
|
+
"Could not load"
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
jest.mock("react-i18next", () => {
|
|
140
|
+
return {
|
|
141
|
+
useTranslation: () => {
|
|
142
|
+
return {
|
|
143
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
144
|
+
return options?.defaultValue ?? key;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* The rule tables are stubbed down to the two facts this file is about: which
|
|
153
|
+
* rule type a table filters on, and which severity band it was mounted for.
|
|
154
|
+
* Rendering the real ModelTable would drag in the pager and the facet bar, none
|
|
155
|
+
* of which says anything about how the pages were split.
|
|
156
|
+
*/
|
|
157
|
+
interface CapturedTableProps {
|
|
158
|
+
query: Record<string, ObjectID | NotificationRuleType | undefined>;
|
|
159
|
+
userPreferencesKey: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let capturedTables: Array<CapturedTableProps> = [];
|
|
163
|
+
|
|
164
|
+
type GetCapturedTablesFunction = () => Array<CapturedTableProps>;
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* The captured list is REPLACED between tests rather than emptied in place, so
|
|
168
|
+
* a closure built inside the `for` loops below would otherwise read whichever
|
|
169
|
+
* array happened to be bound when the loop ran. Going through a stable function
|
|
170
|
+
* makes each test see the list as it stands when that test executes, which is
|
|
171
|
+
* also what stops eslint's no-loop-func from being right about it.
|
|
172
|
+
*/
|
|
173
|
+
const getCapturedTables: GetCapturedTablesFunction =
|
|
174
|
+
(): Array<CapturedTableProps> => {
|
|
175
|
+
return capturedTables;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type TableIdentityFunction = (props: CapturedTableProps) => string;
|
|
179
|
+
|
|
180
|
+
const tableIdentity: TableIdentityFunction = (
|
|
181
|
+
props: CapturedTableProps,
|
|
182
|
+
): string => {
|
|
183
|
+
const severityId: ObjectID | NotificationRuleType | undefined =
|
|
184
|
+
props.query["incidentSeverityId"] ?? props.query["alertSeverityId"];
|
|
185
|
+
|
|
186
|
+
return `${String(props.query["ruleType"])}::${
|
|
187
|
+
severityId ? severityId.toString() : "no-severity"
|
|
188
|
+
}`;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
|
|
192
|
+
return {
|
|
193
|
+
__esModule: true,
|
|
194
|
+
default: (props: CapturedTableProps) => {
|
|
195
|
+
const identity: string = tableIdentity(props);
|
|
196
|
+
|
|
197
|
+
const existingIndex: number = capturedTables.findIndex(
|
|
198
|
+
(candidate: CapturedTableProps): boolean => {
|
|
199
|
+
return tableIdentity(candidate) === identity;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (existingIndex === -1) {
|
|
204
|
+
capturedTables.push(props);
|
|
205
|
+
} else {
|
|
206
|
+
capturedTables[existingIndex] = props;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
import UserViewNotificationRulesRedirect from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/NotificationRules";
|
|
215
|
+
import UserViewSideMenu from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/SideMenu";
|
|
216
|
+
import UserViewOnCallLayout from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/Layout";
|
|
217
|
+
import UserViewNotificationMethods from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/NotificationMethods";
|
|
218
|
+
import UserViewOnCallReadiness from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/Readiness";
|
|
219
|
+
import UserViewOnCallRules, {
|
|
220
|
+
ALERT_EPISODE_RULES_PROPS,
|
|
221
|
+
ALERT_RULES_PROPS,
|
|
222
|
+
INCIDENT_EPISODE_RULES_PROPS,
|
|
223
|
+
INCIDENT_RULES_PROPS,
|
|
224
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Pages/Users/View/OnCall/Rules";
|
|
225
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
226
|
+
import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
|
|
227
|
+
import RouteMap, {
|
|
228
|
+
RouteUtil,
|
|
229
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
|
|
230
|
+
import { getUsersBreadcrumbs } from "../../../../App/FeatureSet/Dashboard/src/Utils/Breadcrumbs/UsersBreadcrumbs";
|
|
231
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
232
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
233
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
234
|
+
import TeamMember from "../../../Models/DatabaseModels/TeamMember";
|
|
235
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
236
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
237
|
+
import Route from "../../../Types/API/Route";
|
|
238
|
+
import Email from "../../../Types/Email";
|
|
239
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
240
|
+
import Link from "../../../Types/Link";
|
|
241
|
+
import Name from "../../../Types/Name";
|
|
242
|
+
import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
|
|
243
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
244
|
+
import Permission from "../../../Types/Permission";
|
|
245
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
246
|
+
import PermissionUtil from "../../../UI/Utils/Permission";
|
|
247
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
248
|
+
import UserUtil from "../../../UI/Utils/User";
|
|
249
|
+
|
|
250
|
+
const PROJECT_ID: ObjectID = new ObjectID(PROJECT_ID_STRING);
|
|
251
|
+
const SIGNED_IN_USER_ID: ObjectID = new ObjectID(SIGNED_IN_USER_ID_STRING);
|
|
252
|
+
const TARGET_USER_ID: ObjectID = new ObjectID(TARGET_USER_ID_STRING);
|
|
253
|
+
|
|
254
|
+
const pageProps: PageComponentProps = {
|
|
255
|
+
pageRoute: new Route("/users"),
|
|
256
|
+
currentProject: null,
|
|
257
|
+
hasPaymentMethod: false,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
interface SeveritySpec {
|
|
261
|
+
id: string;
|
|
262
|
+
name: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const INCIDENT_SEVERITY_SPECS: Array<SeveritySpec> = [
|
|
266
|
+
{ id: INCIDENT_SEVERITY_ONE_ID, name: "Sev One" },
|
|
267
|
+
{ id: INCIDENT_SEVERITY_TWO_ID, name: "Sev Two" },
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const ALERT_SEVERITY_SPECS: Array<SeveritySpec> = [
|
|
271
|
+
{ id: ALERT_SEVERITY_ONE_ID, name: "Alert One" },
|
|
272
|
+
{ id: ALERT_SEVERITY_TWO_ID, name: "Alert Two" },
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
type BuildSeverities = (
|
|
276
|
+
modelType: { new (): IncidentSeverity | AlertSeverity },
|
|
277
|
+
specs: Array<SeveritySpec>,
|
|
278
|
+
) => Array<IncidentSeverity | AlertSeverity>;
|
|
279
|
+
|
|
280
|
+
const buildSeverities: BuildSeverities = (
|
|
281
|
+
modelType: { new (): IncidentSeverity | AlertSeverity },
|
|
282
|
+
specs: Array<SeveritySpec>,
|
|
283
|
+
): Array<IncidentSeverity | AlertSeverity> => {
|
|
284
|
+
return specs.map((spec: SeveritySpec) => {
|
|
285
|
+
const severity: IncidentSeverity | AlertSeverity = new modelType();
|
|
286
|
+
severity._id = spec.id;
|
|
287
|
+
severity.name = spec.name;
|
|
288
|
+
return severity;
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
type BuildTeamMemberFunction = () => TeamMember;
|
|
293
|
+
|
|
294
|
+
const buildTeamMember: BuildTeamMemberFunction = (): TeamMember => {
|
|
295
|
+
const user: User = new User();
|
|
296
|
+
user._id = TARGET_USER_ID_STRING;
|
|
297
|
+
user.name = new Name(TARGET_USER_NAME);
|
|
298
|
+
user.email = new Email(TARGET_LOGIN_EMAIL);
|
|
299
|
+
|
|
300
|
+
const member: TeamMember = new TeamMember();
|
|
301
|
+
member.user = user;
|
|
302
|
+
|
|
303
|
+
return member;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const READINESS_PAYLOAD: JSONObject = {
|
|
307
|
+
userId: TARGET_USER_ID_STRING,
|
|
308
|
+
userName: TARGET_USER_NAME,
|
|
309
|
+
userEmail: TARGET_LOGIN_EMAIL,
|
|
310
|
+
status: "PartiallyReady",
|
|
311
|
+
methods: [],
|
|
312
|
+
coverage: [],
|
|
313
|
+
reasons: [],
|
|
314
|
+
reachedVia: ["Team"],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/*
|
|
318
|
+
* The six pages of the section, as the ROUTE TABLE spells them, paired with the
|
|
319
|
+
* component each route mounts. Everything below reads this rather than a
|
|
320
|
+
* hand-written list of paths, so a page whose route is renamed is a compile
|
|
321
|
+
* error or a failing render rather than a test that quietly stops covering it.
|
|
322
|
+
*/
|
|
323
|
+
interface SectionPage {
|
|
324
|
+
pageMapKey: PageMap;
|
|
325
|
+
menuTitle: string;
|
|
326
|
+
breadcrumbTitle: string;
|
|
327
|
+
element: ReactElement;
|
|
328
|
+
/* The rule type this page is expected to mount tables for, if any. */
|
|
329
|
+
ruleType?: NotificationRuleType | undefined;
|
|
330
|
+
/* The severity ids the tables on this page must be banded by. */
|
|
331
|
+
severityIds?: Array<string> | undefined;
|
|
332
|
+
/* Ids from the OTHER severity model, which must never appear on it. */
|
|
333
|
+
foreignSeverityIds?: Array<string> | undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const SECTION_PAGES: Array<SectionPage> = [
|
|
337
|
+
{
|
|
338
|
+
pageMapKey: PageMap.USER_VIEW_ON_CALL_READINESS,
|
|
339
|
+
menuTitle: "Readiness",
|
|
340
|
+
breadcrumbTitle: "On-Call Readiness",
|
|
341
|
+
element: <UserViewOnCallReadiness {...pageProps} />,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
pageMapKey: PageMap.USER_VIEW_NOTIFICATION_METHODS,
|
|
345
|
+
menuTitle: "Notification Methods",
|
|
346
|
+
breadcrumbTitle: "Notification Methods",
|
|
347
|
+
element: <UserViewNotificationMethods {...pageProps} />,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
pageMapKey: PageMap.USER_VIEW_INCIDENT_ON_CALL_RULES,
|
|
351
|
+
menuTitle: "Incident On-Call Rules",
|
|
352
|
+
breadcrumbTitle: "Incident On-Call Rules",
|
|
353
|
+
element: <UserViewOnCallRules {...INCIDENT_RULES_PROPS} />,
|
|
354
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
355
|
+
severityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
|
|
356
|
+
foreignSeverityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
pageMapKey: PageMap.USER_VIEW_INCIDENT_EPISODE_ON_CALL_RULES,
|
|
360
|
+
menuTitle: "Incident Episode On-Call Rules",
|
|
361
|
+
breadcrumbTitle: "Incident Episode On-Call Rules",
|
|
362
|
+
element: <UserViewOnCallRules {...INCIDENT_EPISODE_RULES_PROPS} />,
|
|
363
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
364
|
+
/*
|
|
365
|
+
* The crossed pair: an incident EPISODE is banded by IncidentSeverity while
|
|
366
|
+
* an alert episode is banded by AlertSeverity. Deriving one axis from the
|
|
367
|
+
* other — "is this an episode?" — gets exactly this row wrong.
|
|
368
|
+
*/
|
|
369
|
+
severityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
|
|
370
|
+
foreignSeverityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
pageMapKey: PageMap.USER_VIEW_ALERT_ON_CALL_RULES,
|
|
374
|
+
menuTitle: "Alert On-Call Rules",
|
|
375
|
+
breadcrumbTitle: "Alert On-Call Rules",
|
|
376
|
+
element: <UserViewOnCallRules {...ALERT_RULES_PROPS} />,
|
|
377
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
378
|
+
severityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
|
|
379
|
+
foreignSeverityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
pageMapKey: PageMap.USER_VIEW_ALERT_EPISODE_ON_CALL_RULES,
|
|
383
|
+
menuTitle: "Alert Episode On-Call Rules",
|
|
384
|
+
breadcrumbTitle: "Alert Episode On-Call Rules",
|
|
385
|
+
element: <UserViewOnCallRules {...ALERT_EPISODE_RULES_PROPS} />,
|
|
386
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
|
387
|
+
severityIds: [ALERT_SEVERITY_ONE_ID, ALERT_SEVERITY_TWO_ID],
|
|
388
|
+
foreignSeverityIds: [INCIDENT_SEVERITY_ONE_ID, INCIDENT_SEVERITY_TWO_ID],
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
type LastPathForFunction = (pageMapKey: PageMap) => string;
|
|
393
|
+
|
|
394
|
+
/*
|
|
395
|
+
* The last URL segment of a page, taken from the app's own route table exactly
|
|
396
|
+
* the way UsersRoutes.tsx takes it. A test that hard-coded "on-call-readiness"
|
|
397
|
+
* would keep passing after somebody renamed the route and broke every link to
|
|
398
|
+
* it.
|
|
399
|
+
*/
|
|
400
|
+
const lastPathFor: LastPathForFunction = (pageMapKey: PageMap): string => {
|
|
401
|
+
return RouteUtil.getLastPathForKey(pageMapKey);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
type RenderPageFunction = (page: SectionPage) => HTMLElement;
|
|
405
|
+
|
|
406
|
+
/*
|
|
407
|
+
* The real nesting: the `:id` parent, the pathless section layout, then the
|
|
408
|
+
* page. The layout reads the target user from `useParams`, so this is also what
|
|
409
|
+
* pins "the section knows whose configuration it is showing" to the URL rather
|
|
410
|
+
* than to a stub.
|
|
411
|
+
*/
|
|
412
|
+
const renderPage: RenderPageFunction = (page: SectionPage): HTMLElement => {
|
|
413
|
+
const path: string = lastPathFor(page.pageMapKey);
|
|
414
|
+
|
|
415
|
+
const { container } = render(
|
|
416
|
+
<MemoryRouter
|
|
417
|
+
initialEntries={[
|
|
418
|
+
`/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/${path}`,
|
|
419
|
+
]}
|
|
420
|
+
>
|
|
421
|
+
<RouterRoutes>
|
|
422
|
+
<RouterRoute
|
|
423
|
+
path="/dashboard/:projectId/users/:id"
|
|
424
|
+
element={<Outlet />}
|
|
425
|
+
>
|
|
426
|
+
<RouterRoute element={<UserViewOnCallLayout />}>
|
|
427
|
+
<RouterRoute path={path} element={page.element} />
|
|
428
|
+
</RouterRoute>
|
|
429
|
+
</RouterRoute>
|
|
430
|
+
</RouterRoutes>
|
|
431
|
+
</MemoryRouter>,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
return container;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
type SettleFunction = () => Promise<void>;
|
|
438
|
+
|
|
439
|
+
/*
|
|
440
|
+
* The two places that read the router's location without being inside one.
|
|
441
|
+
*
|
|
442
|
+
* SideMenu asks Navigation.isOnThisPage to decide which entry is active, and
|
|
443
|
+
* the breadcrumb builder splits the current pathname to work out the levels
|
|
444
|
+
* above. In the app the router pushes that location into Navigation on every
|
|
445
|
+
* navigation; nothing does so in a test, and the getters throw on undefined
|
|
446
|
+
* rather than returning a default — deliberately, since a menu that silently
|
|
447
|
+
* decided nothing was active would be a much quieter bug.
|
|
448
|
+
*/
|
|
449
|
+
type GoToFunction = (path: string) => void;
|
|
450
|
+
|
|
451
|
+
const goTo: GoToFunction = (path: string): void => {
|
|
452
|
+
window.history.pushState({}, "", path);
|
|
453
|
+
Navigation.setLocation({
|
|
454
|
+
pathname: path,
|
|
455
|
+
search: "",
|
|
456
|
+
hash: "",
|
|
457
|
+
state: null,
|
|
458
|
+
key: "test",
|
|
459
|
+
} as Location);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const settle: SettleFunction = async (): Promise<void> => {
|
|
463
|
+
await waitFor(
|
|
464
|
+
(): void => {
|
|
465
|
+
expect(pendingRequestCount).toBe(0);
|
|
466
|
+
},
|
|
467
|
+
{ timeout: 4000 },
|
|
468
|
+
);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
beforeEach((): void => {
|
|
472
|
+
capturedTables = [];
|
|
473
|
+
pendingRequestCount = 0;
|
|
474
|
+
|
|
475
|
+
getListMock.mockReset();
|
|
476
|
+
getItemMock.mockReset();
|
|
477
|
+
getCommonHeadersMock.mockReset();
|
|
478
|
+
apiGetMock.mockReset();
|
|
479
|
+
navigateMock.mockReset();
|
|
480
|
+
|
|
481
|
+
getListMock.mockImplementation((data: any) => {
|
|
482
|
+
if (data.modelType === IncidentSeverity) {
|
|
483
|
+
return Promise.resolve({
|
|
484
|
+
data: buildSeverities(IncidentSeverity, INCIDENT_SEVERITY_SPECS),
|
|
485
|
+
count: INCIDENT_SEVERITY_SPECS.length,
|
|
486
|
+
skip: 0,
|
|
487
|
+
limit: INCIDENT_SEVERITY_SPECS.length,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (data.modelType === AlertSeverity) {
|
|
492
|
+
return Promise.resolve({
|
|
493
|
+
data: buildSeverities(AlertSeverity, ALERT_SEVERITY_SPECS),
|
|
494
|
+
count: ALERT_SEVERITY_SPECS.length,
|
|
495
|
+
skip: 0,
|
|
496
|
+
limit: ALERT_SEVERITY_SPECS.length,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (data.modelType === TeamMember) {
|
|
501
|
+
const member: TeamMember = buildTeamMember();
|
|
502
|
+
return Promise.resolve({ data: [member], count: 1, skip: 0, limit: 1 });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return Promise.resolve({ data: [], count: 0, skip: 0, limit: 0 });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const project: Project = new Project();
|
|
509
|
+
project.disableOnCallNotificationFallback = false;
|
|
510
|
+
getItemMock.mockResolvedValue(project as never);
|
|
511
|
+
|
|
512
|
+
getCommonHeadersMock.mockReturnValue({} as never);
|
|
513
|
+
apiGetMock.mockResolvedValue(
|
|
514
|
+
new HTTPResponse<JSONObject>(200, READINESS_PAYLOAD, {}) as never,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
goTo(
|
|
518
|
+
`/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/on-call-readiness`,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
522
|
+
jest.spyOn(UserUtil, "getUserId").mockReturnValue(SIGNED_IN_USER_ID);
|
|
523
|
+
jest.spyOn(UserUtil, "isMasterAdmin").mockReturnValue(false);
|
|
524
|
+
jest
|
|
525
|
+
.spyOn(PermissionUtil, "getAllPermissions")
|
|
526
|
+
.mockReturnValue([Permission.ProjectAdmin]);
|
|
527
|
+
jest.spyOn(Navigation, "navigate").mockImplementation(((
|
|
528
|
+
...args: Array<unknown>
|
|
529
|
+
): void => {
|
|
530
|
+
navigateMock(...args);
|
|
531
|
+
}) as never);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
afterEach(async (): Promise<void> => {
|
|
535
|
+
cleanup();
|
|
536
|
+
|
|
537
|
+
for (
|
|
538
|
+
let attempt: number = 0;
|
|
539
|
+
pendingRequestCount > 0 && attempt < 100;
|
|
540
|
+
attempt++
|
|
541
|
+
) {
|
|
542
|
+
await new Promise<void>((resolve: () => void): void => {
|
|
543
|
+
setTimeout(resolve, 0);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
jest.restoreAllMocks();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe("the old combined route", () => {
|
|
551
|
+
test("still resolves, and lands on the readiness overview", async () => {
|
|
552
|
+
window.history.pushState(
|
|
553
|
+
{},
|
|
554
|
+
"",
|
|
555
|
+
`/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/notification-rules`,
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
render(<UserViewNotificationRulesRedirect {...pageProps} />);
|
|
559
|
+
|
|
560
|
+
await waitFor((): void => {
|
|
561
|
+
expect(navigateMock).toHaveBeenCalled();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const destination: Route = navigateMock.mock.calls[0]![0] as Route;
|
|
565
|
+
|
|
566
|
+
/*
|
|
567
|
+
* Asserted against the route table rather than against a literal, and with
|
|
568
|
+
* the TARGET user's id in it: the id is the second-to-last segment of the
|
|
569
|
+
* old URL, and reading the wrong offset produces a redirect to a page about
|
|
570
|
+
* the literal string "notification-rules".
|
|
571
|
+
*/
|
|
572
|
+
expect(destination.toString()).toBe(
|
|
573
|
+
RouteUtil.populateRouteParams(
|
|
574
|
+
RouteMap[PageMap.USER_VIEW_ON_CALL_READINESS] as Route,
|
|
575
|
+
{ modelId: TARGET_USER_ID },
|
|
576
|
+
).toString(),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
expect(destination.toString()).toContain(TARGET_USER_ID_STRING);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("reads nothing about the user on its way past", async () => {
|
|
583
|
+
window.history.pushState(
|
|
584
|
+
{},
|
|
585
|
+
"",
|
|
586
|
+
`/dashboard/${PROJECT_ID_STRING}/users/${TARGET_USER_ID_STRING}/notification-rules`,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
render(<UserViewNotificationRulesRedirect {...pageProps} />);
|
|
590
|
+
|
|
591
|
+
await waitFor((): void => {
|
|
592
|
+
expect(navigateMock).toHaveBeenCalled();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
/*
|
|
596
|
+
* A redirect that also fetches is a redirect that has already paid for the
|
|
597
|
+
* page it is not going to draw — and on this surface the thing being
|
|
598
|
+
* fetched is somebody else's paging configuration.
|
|
599
|
+
*/
|
|
600
|
+
expect(apiGetMock).not.toHaveBeenCalled();
|
|
601
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
describe("each route renders its own page and nothing else", () => {
|
|
606
|
+
for (const page of SECTION_PAGES) {
|
|
607
|
+
if (!page.ruleType) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
test(`${page.menuTitle} mounts tables for one rule type only`, async () => {
|
|
612
|
+
renderPage(page);
|
|
613
|
+
|
|
614
|
+
await settle();
|
|
615
|
+
|
|
616
|
+
await waitFor((): void => {
|
|
617
|
+
expect(getCapturedTables().length).toBeGreaterThan(0);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
/*
|
|
621
|
+
* The point of the split, counted. The route this page replaced mounted
|
|
622
|
+
* eight tables — four rule types times two severity bands — and every one
|
|
623
|
+
* of them was on screen at once.
|
|
624
|
+
*/
|
|
625
|
+
expect(getCapturedTables()).toHaveLength(page.severityIds!.length);
|
|
626
|
+
|
|
627
|
+
for (const table of getCapturedTables()) {
|
|
628
|
+
expect(table.query["ruleType"]).toBe(page.ruleType);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test(`${page.menuTitle} bands its tables by the right severity model`, async () => {
|
|
633
|
+
renderPage(page);
|
|
634
|
+
|
|
635
|
+
await settle();
|
|
636
|
+
|
|
637
|
+
await waitFor((): void => {
|
|
638
|
+
expect(getCapturedTables()).toHaveLength(page.severityIds!.length);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const bandedIds: Array<string> = getCapturedTables().map(
|
|
642
|
+
(table: CapturedTableProps): string => {
|
|
643
|
+
const severityId: ObjectID | NotificationRuleType | undefined =
|
|
644
|
+
table.query["incidentSeverityId"] ?? table.query["alertSeverityId"];
|
|
645
|
+
|
|
646
|
+
return severityId ? severityId.toString() : "";
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
expect(bandedIds.sort()).toEqual([...page.severityIds!].sort());
|
|
651
|
+
|
|
652
|
+
/*
|
|
653
|
+
* The disjoint fixture doing its job: a page handed the other model's
|
|
654
|
+
* props would list these ids instead, and nothing else in the render
|
|
655
|
+
* would look wrong.
|
|
656
|
+
*/
|
|
657
|
+
for (const foreignId of page.foreignSeverityIds!) {
|
|
658
|
+
expect(bandedIds).not.toContain(foreignId);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
test("the readiness page mounts no rule table at all", async () => {
|
|
664
|
+
renderPage(SECTION_PAGES[0]!);
|
|
665
|
+
|
|
666
|
+
await settle();
|
|
667
|
+
|
|
668
|
+
/*
|
|
669
|
+
* The overview diagnoses and does not repair. A readiness page that also
|
|
670
|
+
* drew the rule tables would be the page this split exists to break up,
|
|
671
|
+
* wearing a new name.
|
|
672
|
+
*/
|
|
673
|
+
expect(capturedTables).toHaveLength(0);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe("the section shares one load of the person it is about", () => {
|
|
678
|
+
test("a rule page reads identity and readiness exactly once", async () => {
|
|
679
|
+
renderPage(SECTION_PAGES[2]!);
|
|
680
|
+
|
|
681
|
+
await settle();
|
|
682
|
+
|
|
683
|
+
const teamMemberReads: number = getListMock.mock.calls.filter(
|
|
684
|
+
(call: Array<any>): boolean => {
|
|
685
|
+
return call[0].modelType === TeamMember;
|
|
686
|
+
},
|
|
687
|
+
).length;
|
|
688
|
+
|
|
689
|
+
expect(teamMemberReads).toBe(1);
|
|
690
|
+
expect(apiGetMock).toHaveBeenCalledTimes(1);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("every page asks about the user in the URL, never the signed-in admin", async () => {
|
|
694
|
+
for (const page of SECTION_PAGES) {
|
|
695
|
+
capturedTables = [];
|
|
696
|
+
getListMock.mockClear();
|
|
697
|
+
apiGetMock.mockClear();
|
|
698
|
+
|
|
699
|
+
renderPage(page);
|
|
700
|
+
|
|
701
|
+
await settle();
|
|
702
|
+
|
|
703
|
+
const memberRead: any = getListMock.mock.calls.find(
|
|
704
|
+
(call: Array<any>): boolean => {
|
|
705
|
+
return call[0].modelType === TeamMember;
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(memberRead[0].query.userId.toString()).toBe(TARGET_USER_ID_STRING);
|
|
710
|
+
expect(memberRead[0].query.userId.toString()).not.toBe(
|
|
711
|
+
SIGNED_IN_USER_ID_STRING,
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
/*
|
|
715
|
+
* And the readiness read too. The two travelling together is what stops
|
|
716
|
+
* one page in the section naming one person while another names somebody
|
|
717
|
+
* else — the state this surface must never be in.
|
|
718
|
+
*/
|
|
719
|
+
const readinessUrl: string = String(apiGetMock.mock.calls[0]![0].url);
|
|
720
|
+
|
|
721
|
+
expect(readinessUrl).toContain(TARGET_USER_ID_STRING);
|
|
722
|
+
|
|
723
|
+
cleanup();
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("the on-behalf-of banner is on every page in the section", async () => {
|
|
728
|
+
for (const page of SECTION_PAGES) {
|
|
729
|
+
const container: HTMLElement = renderPage(page);
|
|
730
|
+
|
|
731
|
+
await settle();
|
|
732
|
+
|
|
733
|
+
/*
|
|
734
|
+
* Rendered by the LAYOUT, so it cannot be forgotten on a page added
|
|
735
|
+
* later. The whole risk of this section is somebody rewriting the wrong
|
|
736
|
+
* person's paging while believing it is their own, and a page reached
|
|
737
|
+
* directly by URL is exactly the one that would arrive without context.
|
|
738
|
+
*/
|
|
739
|
+
expect(container.textContent).toContain(
|
|
740
|
+
`You are editing on behalf of ${TARGET_USER_NAME}`,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
cleanup();
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("a reader with no permission is refused on every page, and nothing is fetched", async () => {
|
|
748
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
749
|
+
|
|
750
|
+
for (const page of SECTION_PAGES) {
|
|
751
|
+
getListMock.mockClear();
|
|
752
|
+
apiGetMock.mockClear();
|
|
753
|
+
|
|
754
|
+
renderPage(page);
|
|
755
|
+
|
|
756
|
+
expect(
|
|
757
|
+
await screen.findByText(/do not have permission to view this user/),
|
|
758
|
+
).toBeInTheDocument();
|
|
759
|
+
|
|
760
|
+
/*
|
|
761
|
+
* Not merely "nothing drawn": nothing requested either. A refused page
|
|
762
|
+
* that still fetches a colleague's readiness has already disclosed the
|
|
763
|
+
* thing it declined to render — and because the refusal lives in the
|
|
764
|
+
* LAYOUT, one check covers all six rather than five plus whichever one
|
|
765
|
+
* somebody forgets.
|
|
766
|
+
*/
|
|
767
|
+
expect(apiGetMock).not.toHaveBeenCalled();
|
|
768
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
769
|
+
|
|
770
|
+
cleanup();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
describe("every page is reachable", () => {
|
|
776
|
+
type MenuLinkFunction = () => Array<{ title: string; href: string }>;
|
|
777
|
+
|
|
778
|
+
const renderMenu: MenuLinkFunction = (): Array<{
|
|
779
|
+
title: string;
|
|
780
|
+
href: string;
|
|
781
|
+
}> => {
|
|
782
|
+
render(
|
|
783
|
+
<MemoryRouter>
|
|
784
|
+
<UserViewSideMenu modelId={TARGET_USER_ID} hasCustomFields={false} />
|
|
785
|
+
</MemoryRouter>,
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
return Array.from(document.querySelectorAll("a")).map(
|
|
789
|
+
(anchor: HTMLAnchorElement): { title: string; href: string } => {
|
|
790
|
+
return {
|
|
791
|
+
title: anchor.textContent?.trim() || "",
|
|
792
|
+
href: anchor.getAttribute("href") || "",
|
|
793
|
+
};
|
|
794
|
+
},
|
|
795
|
+
);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
test("the side menu names all six pages, pointing at the route table's own paths", () => {
|
|
799
|
+
const links: Array<{ title: string; href: string }> = renderMenu();
|
|
800
|
+
|
|
801
|
+
for (const page of SECTION_PAGES) {
|
|
802
|
+
const expectedHref: string = RouteUtil.populateRouteParams(
|
|
803
|
+
RouteMap[page.pageMapKey] as Route,
|
|
804
|
+
{ modelId: TARGET_USER_ID },
|
|
805
|
+
).toString();
|
|
806
|
+
|
|
807
|
+
const link: { title: string; href: string } | undefined = links.find(
|
|
808
|
+
(candidate: { title: string; href: string }): boolean => {
|
|
809
|
+
return candidate.title === page.menuTitle;
|
|
810
|
+
},
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
expect(link).toBeDefined();
|
|
814
|
+
expect(link!.href).toBe(expectedHref);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("the menu hides the section from somebody who may not read it", () => {
|
|
819
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
820
|
+
|
|
821
|
+
const links: Array<{ title: string; href: string }> = renderMenu();
|
|
822
|
+
|
|
823
|
+
for (const page of SECTION_PAGES) {
|
|
824
|
+
expect(
|
|
825
|
+
links.some((candidate: { title: string; href: string }): boolean => {
|
|
826
|
+
return candidate.title === page.menuTitle;
|
|
827
|
+
}),
|
|
828
|
+
).toBe(false);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/*
|
|
832
|
+
* Hiding is a convenience and never the boundary — the pages repeat the
|
|
833
|
+
* check and the API refuses the reads — but a menu full of entries that all
|
|
834
|
+
* refuse is its own kind of broken.
|
|
835
|
+
*/
|
|
836
|
+
expect(
|
|
837
|
+
links.some((candidate: { title: string; href: string }): boolean => {
|
|
838
|
+
return candidate.title === "Profile";
|
|
839
|
+
}),
|
|
840
|
+
).toBe(true);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test("a member walking into their own row keeps the section", () => {
|
|
844
|
+
jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
|
|
845
|
+
jest.spyOn(UserUtil, "getUserId").mockReturnValue(TARGET_USER_ID);
|
|
846
|
+
|
|
847
|
+
const links: Array<{ title: string; href: string }> = renderMenu();
|
|
848
|
+
|
|
849
|
+
/*
|
|
850
|
+
* Your own configuration needs no grant at all — Permission.CurrentUser
|
|
851
|
+
* already carries it — so an ordinary member reaching their own row sees
|
|
852
|
+
* the same six pages.
|
|
853
|
+
*/
|
|
854
|
+
for (const page of SECTION_PAGES) {
|
|
855
|
+
expect(
|
|
856
|
+
links.some((candidate: { title: string; href: string }): boolean => {
|
|
857
|
+
return candidate.title === page.menuTitle;
|
|
858
|
+
}),
|
|
859
|
+
).toBe(true);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("every page has a breadcrumb of its own", () => {
|
|
864
|
+
for (const page of SECTION_PAGES) {
|
|
865
|
+
const route: Route = RouteMap[page.pageMapKey] as Route;
|
|
866
|
+
|
|
867
|
+
const breadcrumbs: Array<Link> | undefined = getUsersBreadcrumbs(
|
|
868
|
+
route.toString(),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
expect(breadcrumbs).toBeDefined();
|
|
872
|
+
|
|
873
|
+
const titles: Array<string> = breadcrumbs!.map((link: Link): string => {
|
|
874
|
+
return link.title;
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
/*
|
|
878
|
+
* A page with no breadcrumb entry renders a bare title and no way back up
|
|
879
|
+
* — survivable on one route, six times worse once a single page became a
|
|
880
|
+
* section somebody navigates around inside.
|
|
881
|
+
*/
|
|
882
|
+
expect(titles).toContain(page.breadcrumbTitle);
|
|
883
|
+
expect(titles[0]).toBe("Project");
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("the six routes are distinct URLs", () => {
|
|
888
|
+
const paths: Array<string> = SECTION_PAGES.map(
|
|
889
|
+
(page: SectionPage): string => {
|
|
890
|
+
return (RouteMap[page.pageMapKey] as Route).toString();
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
/*
|
|
895
|
+
* Two PageMap keys pointing at one path is a copy-paste that produces a
|
|
896
|
+
* menu entry which silently opens somebody else's page.
|
|
897
|
+
*/
|
|
898
|
+
expect(new Set(paths).size).toBe(SECTION_PAGES.length);
|
|
899
|
+
|
|
900
|
+
// And none of them collides with the legacy route that now redirects.
|
|
901
|
+
expect(paths).not.toContain(
|
|
902
|
+
(RouteMap[PageMap.USER_VIEW_NOTIFICATION_RULES] as Route).toString(),
|
|
903
|
+
);
|
|
904
|
+
});
|
|
905
|
+
});
|