@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,948 @@
|
|
|
1
|
+
import UserNotificationMethodAdminService, {
|
|
2
|
+
AdminNotificationMethodView,
|
|
3
|
+
} from "../../../Server/Services/UserNotificationMethodAdminService";
|
|
4
|
+
import OnCallReadinessService, {
|
|
5
|
+
IDENTIFIER_MASK,
|
|
6
|
+
ReadinessMethodType,
|
|
7
|
+
} from "../../../Server/Services/OnCallReadinessService";
|
|
8
|
+
import MailService from "../../../Server/Services/MailService";
|
|
9
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
10
|
+
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
11
|
+
import UserCallService from "../../../Server/Services/UserCallService";
|
|
12
|
+
import UserEmailService from "../../../Server/Services/UserEmailService";
|
|
13
|
+
import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
|
|
14
|
+
import UserPushService from "../../../Server/Services/UserPushService";
|
|
15
|
+
import UserService from "../../../Server/Services/UserService";
|
|
16
|
+
import UserSmsService from "../../../Server/Services/UserSmsService";
|
|
17
|
+
import UserTelegramService from "../../../Server/Services/UserTelegramService";
|
|
18
|
+
import UserWebhookService from "../../../Server/Services/UserWebhookService";
|
|
19
|
+
import UserWhatsAppService from "../../../Server/Services/UserWhatsAppService";
|
|
20
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
21
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
22
|
+
import UserCall from "../../../Models/DatabaseModels/UserCall";
|
|
23
|
+
import UserEmail from "../../../Models/DatabaseModels/UserEmail";
|
|
24
|
+
import UserPush from "../../../Models/DatabaseModels/UserPush";
|
|
25
|
+
import UserSMS from "../../../Models/DatabaseModels/UserSMS";
|
|
26
|
+
import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
|
|
27
|
+
import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
|
|
28
|
+
import UserWhatsApp from "../../../Models/DatabaseModels/UserWhatsApp";
|
|
29
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
30
|
+
import Email from "../../../Types/Email";
|
|
31
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
32
|
+
import Name from "../../../Types/Name";
|
|
33
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
34
|
+
import Phone from "../../../Types/Phone";
|
|
35
|
+
import { beforeEach, describe, expect, test } from "@jest/globals";
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* The service that lets a project administrator set up somebody else's
|
|
39
|
+
* notification methods.
|
|
40
|
+
*
|
|
41
|
+
* Everything here is tested against the REAL masking, not a stub of it. That is
|
|
42
|
+
* the whole point of testing this layer separately from the router: the claim
|
|
43
|
+
* is that a raw phone number sitting in the database cannot come back out of
|
|
44
|
+
* this service, and the only way to test that claim is to put a raw phone
|
|
45
|
+
* number in one end and read what comes out of the other. A masking test that
|
|
46
|
+
* stubbed the masker proves nothing.
|
|
47
|
+
*
|
|
48
|
+
* The properties this file exists to pin, in the order they matter:
|
|
49
|
+
*
|
|
50
|
+
* 1. AN ADMIN-ADDED METHOD IS NOT LIVE. The row is written with the TARGET's
|
|
51
|
+
* user id and with isVerified untouched, which is what makes the method
|
|
52
|
+
* service send a verification code to the address itself. Nothing in this
|
|
53
|
+
* service can mark a method verified, and neither can the administrator
|
|
54
|
+
* afterwards — the verify endpoints compare the row's owner against the
|
|
55
|
+
* signed-in caller. If a future edit sets isVerified, or writes the
|
|
56
|
+
* ACTOR's user id instead of the target's, an administrator's own phone
|
|
57
|
+
* becomes a live delivery address on a colleague's account. Both are
|
|
58
|
+
* asserted on the row that is actually handed to the create.
|
|
59
|
+
*
|
|
60
|
+
* 2. NOTHING RAW COMES BACK. Every view this service returns goes through one
|
|
61
|
+
* function, and these tests scan the whole returned structure for the raw
|
|
62
|
+
* values they planted rather than checking a field by name.
|
|
63
|
+
*
|
|
64
|
+
* 3. THE OWNER IS ALWAYS TOLD. An admin adding a colleague's work phone and
|
|
65
|
+
* an attacker adding a number of their own are the same request from the
|
|
66
|
+
* server's side. The only party who always knows which it was is the
|
|
67
|
+
* account holder, so the mail is not decoration — it is the control.
|
|
68
|
+
*
|
|
69
|
+
* 4. MEMBERSHIP IS A CLAIM ABOUT A PROJECT. Holding an administrative
|
|
70
|
+
* permission somewhere is not a licence to write for a user id anywhere.
|
|
71
|
+
*
|
|
72
|
+
* 5. THE THREE UNADDABLE CHANNELS STAY UNADDABLE. Push has no value to type,
|
|
73
|
+
* Telegram needs the account holder to talk to the bot, and a webhook
|
|
74
|
+
* would be live the instant it was written because UserWebhook has no
|
|
75
|
+
* verification at all.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
const RAW_EMAIL: string = "jane.ops@example.com";
|
|
79
|
+
const RAW_PHONE: string = "+14155554821";
|
|
80
|
+
const RAW_TELEGRAM_HANDLE: string = "@janeops_oncall";
|
|
81
|
+
const RAW_WEBHOOK_NAME: string = "payments-hook";
|
|
82
|
+
const RAW_DEVICE_NAME: string = "Jane's iPhone";
|
|
83
|
+
|
|
84
|
+
const ALL_RAW_VALUES: Array<string> = [
|
|
85
|
+
RAW_EMAIL,
|
|
86
|
+
RAW_PHONE,
|
|
87
|
+
RAW_TELEGRAM_HANDLE,
|
|
88
|
+
RAW_WEBHOOK_NAME,
|
|
89
|
+
RAW_DEVICE_NAME,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
let projectId: ObjectID;
|
|
93
|
+
let targetUserId: ObjectID;
|
|
94
|
+
let actorUserId: ObjectID;
|
|
95
|
+
let emailMethodId: ObjectID;
|
|
96
|
+
let smsMethodId: ObjectID;
|
|
97
|
+
|
|
98
|
+
let teamMemberFindOneBy: jest.SpyInstance;
|
|
99
|
+
let mailSpy: jest.SpyInstance;
|
|
100
|
+
let clearCacheSpy: jest.SpyInstance;
|
|
101
|
+
|
|
102
|
+
let emailFindBy: jest.SpyInstance;
|
|
103
|
+
let smsFindBy: jest.SpyInstance;
|
|
104
|
+
let callFindBy: jest.SpyInstance;
|
|
105
|
+
let whatsAppFindBy: jest.SpyInstance;
|
|
106
|
+
let pushFindBy: jest.SpyInstance;
|
|
107
|
+
let telegramFindBy: jest.SpyInstance;
|
|
108
|
+
let webhookFindBy: jest.SpyInstance;
|
|
109
|
+
|
|
110
|
+
let emailCreate: jest.SpyInstance;
|
|
111
|
+
let smsCreate: jest.SpyInstance;
|
|
112
|
+
let smsFindOneBy: jest.SpyInstance;
|
|
113
|
+
let smsDelete: jest.SpyInstance;
|
|
114
|
+
let smsResend: jest.SpyInstance;
|
|
115
|
+
let deletionImpact: jest.SpyInstance;
|
|
116
|
+
|
|
117
|
+
function props(): DatabaseCommonInteractionProps {
|
|
118
|
+
return {
|
|
119
|
+
tenantId: projectId,
|
|
120
|
+
userId: actorUserId,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Every string anywhere in a returned structure, however deeply nested. */
|
|
125
|
+
function everyStringIn(value: unknown): Array<string> {
|
|
126
|
+
if (typeof value === "string") {
|
|
127
|
+
return [value];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (value instanceof ObjectID || value instanceof Date) {
|
|
131
|
+
return [value.toString()];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
return value.flatMap(everyStringIn);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (value && typeof value === "object") {
|
|
139
|
+
return Object.values(value as Record<string, unknown>).flatMap(
|
|
140
|
+
everyStringIn,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function expectNothingRawIn(value: unknown): void {
|
|
148
|
+
const strings: string = everyStringIn(value).join(" || ");
|
|
149
|
+
|
|
150
|
+
for (const raw of ALL_RAW_VALUES) {
|
|
151
|
+
expect(strings).not.toContain(raw);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildEmailRow(): UserEmail {
|
|
156
|
+
const row: UserEmail = new UserEmail();
|
|
157
|
+
row._id = emailMethodId.toString();
|
|
158
|
+
row.projectId = projectId;
|
|
159
|
+
row.userId = targetUserId;
|
|
160
|
+
row.email = new Email(RAW_EMAIL);
|
|
161
|
+
row.isVerified = true;
|
|
162
|
+
return row;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildSmsRow(): UserSMS {
|
|
166
|
+
const row: UserSMS = new UserSMS();
|
|
167
|
+
row._id = smsMethodId.toString();
|
|
168
|
+
row.projectId = projectId;
|
|
169
|
+
row.userId = targetUserId;
|
|
170
|
+
row.phone = new Phone(RAW_PHONE);
|
|
171
|
+
row.isVerified = false;
|
|
172
|
+
return row;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildTelegramRow(): UserTelegram {
|
|
176
|
+
const row: UserTelegram = new UserTelegram();
|
|
177
|
+
row._id = ObjectID.generate().toString();
|
|
178
|
+
row.projectId = projectId;
|
|
179
|
+
row.userId = targetUserId;
|
|
180
|
+
row.telegramUserHandle = RAW_TELEGRAM_HANDLE;
|
|
181
|
+
row.isVerified = true;
|
|
182
|
+
return row;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildWebhookRow(): UserWebhook {
|
|
186
|
+
const row: UserWebhook = new UserWebhook();
|
|
187
|
+
row._id = ObjectID.generate().toString();
|
|
188
|
+
row.projectId = projectId;
|
|
189
|
+
row.userId = targetUserId;
|
|
190
|
+
row.name = RAW_WEBHOOK_NAME;
|
|
191
|
+
return row;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildPushRow(): UserPush {
|
|
195
|
+
const row: UserPush = new UserPush();
|
|
196
|
+
row._id = ObjectID.generate().toString();
|
|
197
|
+
row.projectId = projectId;
|
|
198
|
+
row.userId = targetUserId;
|
|
199
|
+
row.deviceName = RAW_DEVICE_NAME;
|
|
200
|
+
row.isVerified = true;
|
|
201
|
+
return row;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function viewFor(
|
|
205
|
+
methods: Array<AdminNotificationMethodView>,
|
|
206
|
+
methodType: ReadinessMethodType,
|
|
207
|
+
): AdminNotificationMethodView {
|
|
208
|
+
const view: AdminNotificationMethodView | undefined = methods.find(
|
|
209
|
+
(candidate: AdminNotificationMethodView): boolean => {
|
|
210
|
+
return candidate.methodType === methodType;
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!view) {
|
|
215
|
+
throw new Error(`no view for ${methodType}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return view;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
jest.restoreAllMocks();
|
|
223
|
+
jest.clearAllMocks();
|
|
224
|
+
|
|
225
|
+
projectId = ObjectID.generate();
|
|
226
|
+
targetUserId = ObjectID.generate();
|
|
227
|
+
actorUserId = ObjectID.generate();
|
|
228
|
+
emailMethodId = ObjectID.generate();
|
|
229
|
+
smsMethodId = ObjectID.generate();
|
|
230
|
+
|
|
231
|
+
teamMemberFindOneBy = jest
|
|
232
|
+
.spyOn(TeamMemberService, "findOneBy")
|
|
233
|
+
.mockResolvedValue({ _id: ObjectID.generate().toString() } as never);
|
|
234
|
+
|
|
235
|
+
mailSpy = jest
|
|
236
|
+
.spyOn(MailService, "sendMail")
|
|
237
|
+
.mockResolvedValue(undefined as never);
|
|
238
|
+
|
|
239
|
+
clearCacheSpy = jest
|
|
240
|
+
.spyOn(OnCallReadinessService, "clearCache")
|
|
241
|
+
.mockImplementation((): void => {
|
|
242
|
+
return undefined;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const owner: User = new User();
|
|
246
|
+
owner._id = targetUserId.toString();
|
|
247
|
+
owner.name = new Name("Jane Ops");
|
|
248
|
+
owner.email = new Email("jane.login@example.com");
|
|
249
|
+
|
|
250
|
+
const actor: User = new User();
|
|
251
|
+
actor._id = actorUserId.toString();
|
|
252
|
+
actor.name = new Name("Alex Admin");
|
|
253
|
+
actor.email = new Email("alex.admin@example.com");
|
|
254
|
+
|
|
255
|
+
jest
|
|
256
|
+
.spyOn(UserService, "findOneById")
|
|
257
|
+
.mockImplementation((data: any): Promise<User | null> => {
|
|
258
|
+
return Promise.resolve(
|
|
259
|
+
data.id.toString() === targetUserId.toString() ? owner : actor,
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const project: Project = new Project();
|
|
264
|
+
project._id = projectId.toString();
|
|
265
|
+
project.name = "Payments";
|
|
266
|
+
|
|
267
|
+
jest.spyOn(ProjectService, "findOneById").mockResolvedValue(project as never);
|
|
268
|
+
|
|
269
|
+
emailFindBy = jest
|
|
270
|
+
.spyOn(UserEmailService, "findBy")
|
|
271
|
+
.mockResolvedValue([buildEmailRow()] as never);
|
|
272
|
+
smsFindBy = jest
|
|
273
|
+
.spyOn(UserSmsService, "findBy")
|
|
274
|
+
.mockResolvedValue([buildSmsRow()] as never);
|
|
275
|
+
callFindBy = jest
|
|
276
|
+
.spyOn(UserCallService, "findBy")
|
|
277
|
+
.mockResolvedValue([] as never);
|
|
278
|
+
whatsAppFindBy = jest
|
|
279
|
+
.spyOn(UserWhatsAppService, "findBy")
|
|
280
|
+
.mockResolvedValue([] as never);
|
|
281
|
+
pushFindBy = jest
|
|
282
|
+
.spyOn(UserPushService, "findBy")
|
|
283
|
+
.mockResolvedValue([buildPushRow()] as never);
|
|
284
|
+
telegramFindBy = jest
|
|
285
|
+
.spyOn(UserTelegramService, "findBy")
|
|
286
|
+
.mockResolvedValue([buildTelegramRow()] as never);
|
|
287
|
+
webhookFindBy = jest
|
|
288
|
+
.spyOn(UserWebhookService, "findBy")
|
|
289
|
+
.mockResolvedValue([buildWebhookRow()] as never);
|
|
290
|
+
|
|
291
|
+
jest
|
|
292
|
+
.spyOn(UserEmailService, "findOneBy")
|
|
293
|
+
.mockResolvedValue(buildEmailRow() as never);
|
|
294
|
+
smsFindOneBy = jest
|
|
295
|
+
.spyOn(UserSmsService, "findOneBy")
|
|
296
|
+
.mockResolvedValue(buildSmsRow() as never);
|
|
297
|
+
jest.spyOn(UserCallService, "findOneBy").mockResolvedValue(null as never);
|
|
298
|
+
jest.spyOn(UserWhatsAppService, "findOneBy").mockResolvedValue(null as never);
|
|
299
|
+
jest.spyOn(UserPushService, "findOneBy").mockResolvedValue(null as never);
|
|
300
|
+
jest.spyOn(UserTelegramService, "findOneBy").mockResolvedValue(null as never);
|
|
301
|
+
jest.spyOn(UserWebhookService, "findOneBy").mockResolvedValue(null as never);
|
|
302
|
+
|
|
303
|
+
emailCreate = jest
|
|
304
|
+
.spyOn(UserEmailService, "create")
|
|
305
|
+
.mockImplementation((data: any): Promise<UserEmail> => {
|
|
306
|
+
const created: UserEmail = data.data as UserEmail;
|
|
307
|
+
created._id = ObjectID.generate().toString();
|
|
308
|
+
return Promise.resolve(created);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
smsCreate = jest
|
|
312
|
+
.spyOn(UserSmsService, "create")
|
|
313
|
+
.mockImplementation((data: any): Promise<UserSMS> => {
|
|
314
|
+
const created: UserSMS = data.data as UserSMS;
|
|
315
|
+
created._id = ObjectID.generate().toString();
|
|
316
|
+
return Promise.resolve(created);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
jest
|
|
320
|
+
.spyOn(UserCallService, "create")
|
|
321
|
+
.mockImplementation((data: any): Promise<UserCall> => {
|
|
322
|
+
const created: UserCall = data.data as UserCall;
|
|
323
|
+
created._id = ObjectID.generate().toString();
|
|
324
|
+
return Promise.resolve(created);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
jest
|
|
328
|
+
.spyOn(UserWhatsAppService, "create")
|
|
329
|
+
.mockImplementation((data: any): Promise<UserWhatsApp> => {
|
|
330
|
+
const created: UserWhatsApp = data.data as UserWhatsApp;
|
|
331
|
+
created._id = ObjectID.generate().toString();
|
|
332
|
+
return Promise.resolve(created);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
smsDelete = jest
|
|
336
|
+
.spyOn(UserSmsService, "deleteOneById")
|
|
337
|
+
.mockResolvedValue(undefined as never);
|
|
338
|
+
|
|
339
|
+
smsResend = jest
|
|
340
|
+
.spyOn(UserSmsService, "resendVerificationCode")
|
|
341
|
+
.mockResolvedValue(undefined as never);
|
|
342
|
+
|
|
343
|
+
deletionImpact = jest
|
|
344
|
+
.spyOn(UserNotificationRuleService, "getNotificationMethodDeletionImpact")
|
|
345
|
+
.mockResolvedValue({
|
|
346
|
+
projectId: projectId,
|
|
347
|
+
userId: targetUserId,
|
|
348
|
+
isOnCallResponder: true,
|
|
349
|
+
reachedVia: [],
|
|
350
|
+
rulesDeletedCount: 4,
|
|
351
|
+
coverageLost: [
|
|
352
|
+
{ ruleType: "x", rulesRemoved: 1 },
|
|
353
|
+
{ ruleType: "y", rulesRemoved: 1 },
|
|
354
|
+
],
|
|
355
|
+
handoffNotificationsLost: [],
|
|
356
|
+
reachability: "NotReachable",
|
|
357
|
+
verifiedMethodCountAfterDeletion: 0,
|
|
358
|
+
isFallbackEnabled: true,
|
|
359
|
+
isTruncated: false,
|
|
360
|
+
} as never);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("listing", () => {
|
|
364
|
+
test("returns every channel, masked, and never a raw value", async () => {
|
|
365
|
+
const methods: Array<AdminNotificationMethodView> =
|
|
366
|
+
await UserNotificationMethodAdminService.listMethodsForUser({
|
|
367
|
+
projectId: projectId,
|
|
368
|
+
userId: targetUserId,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(methods).toHaveLength(5);
|
|
372
|
+
|
|
373
|
+
/*
|
|
374
|
+
* The real maskIdentifier, not a stub, so these are the exact shapes an
|
|
375
|
+
* administrator sees on screen.
|
|
376
|
+
*/
|
|
377
|
+
expect(viewFor(methods, ReadinessMethodType.Email).maskedIdentifier).toBe(
|
|
378
|
+
`j${IDENTIFIER_MASK}@example.com`,
|
|
379
|
+
);
|
|
380
|
+
expect(viewFor(methods, ReadinessMethodType.SMS).maskedIdentifier).toBe(
|
|
381
|
+
`+1 ${IDENTIFIER_MASK} ${IDENTIFIER_MASK} 4821`,
|
|
382
|
+
);
|
|
383
|
+
expect(
|
|
384
|
+
viewFor(methods, ReadinessMethodType.Telegram).maskedIdentifier,
|
|
385
|
+
).toBe(`@ja${IDENTIFIER_MASK}`);
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* Scanned as a whole rather than field by field, so a view that grew a raw
|
|
389
|
+
* field this test never thought of still fails.
|
|
390
|
+
*/
|
|
391
|
+
expectNothingRawIn(methods);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("selects no column that carries a credential", async () => {
|
|
395
|
+
await UserNotificationMethodAdminService.listMethodsForUser({
|
|
396
|
+
projectId: projectId,
|
|
397
|
+
userId: targetUserId,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
/*
|
|
401
|
+
* UserWebhook.webhookUrl is a bearer credential — anyone holding a
|
|
402
|
+
* Slack/Discord/Teams hook url can post as the integration — and
|
|
403
|
+
* UserPush.deviceToken addresses the device directly. Neither may be
|
|
404
|
+
* selected on this path at all: a value that is never read cannot be
|
|
405
|
+
* leaked by a later serialiser.
|
|
406
|
+
*/
|
|
407
|
+
const webhookSelect: any = webhookFindBy.mock.calls[0]![0].select;
|
|
408
|
+
expect(webhookSelect.webhookUrl).toBeUndefined();
|
|
409
|
+
expect(webhookSelect.secret).toBeUndefined();
|
|
410
|
+
|
|
411
|
+
const pushSelect: any = pushFindBy.mock.calls[0]![0].select;
|
|
412
|
+
expect(pushSelect.deviceToken).toBeUndefined();
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* And the telegram CHAT ID, which is the addressable target a bot sends to.
|
|
416
|
+
* The handle is the human-facing label and is the one an owner recognises.
|
|
417
|
+
*/
|
|
418
|
+
const telegramSelect: any = telegramFindBy.mock.calls[0]![0].select;
|
|
419
|
+
expect(telegramSelect.telegramChatId).toBeUndefined();
|
|
420
|
+
expect(telegramSelect.telegramUserHandle).toBe(true);
|
|
421
|
+
|
|
422
|
+
// Verification codes are live account-takeover material. Never selected.
|
|
423
|
+
for (const spy of [emailFindBy, smsFindBy, callFindBy, whatsAppFindBy]) {
|
|
424
|
+
const select: any = spy.mock.calls[0]![0].select;
|
|
425
|
+
expect(select.verificationCode).toBeUndefined();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("reads only this user's rows, in this project", async () => {
|
|
430
|
+
await UserNotificationMethodAdminService.listMethodsForUser({
|
|
431
|
+
projectId: projectId,
|
|
432
|
+
userId: targetUserId,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
for (const spy of [emailFindBy, smsFindBy, telegramFindBy, webhookFindBy]) {
|
|
436
|
+
const call: any = spy.mock.calls[0]![0];
|
|
437
|
+
|
|
438
|
+
expect(call.query.projectId.toString()).toBe(projectId.toString());
|
|
439
|
+
expect(call.query.userId.toString()).toBe(targetUserId.toString());
|
|
440
|
+
|
|
441
|
+
/*
|
|
442
|
+
* isRoot, necessarily: the models are scoped to their owner, so a
|
|
443
|
+
* non-root read on behalf of an administrator would be refused. It is
|
|
444
|
+
* also exactly why the router above this is the only gate there is.
|
|
445
|
+
*/
|
|
446
|
+
expect(call.props.isRoot).toBe(true);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("marks the three channels an administrator cannot create", async () => {
|
|
451
|
+
const methods: Array<AdminNotificationMethodView> =
|
|
452
|
+
await UserNotificationMethodAdminService.listMethodsForUser({
|
|
453
|
+
projectId: projectId,
|
|
454
|
+
userId: targetUserId,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(viewFor(methods, ReadinessMethodType.Email).isAdminAddable).toBe(
|
|
458
|
+
true,
|
|
459
|
+
);
|
|
460
|
+
expect(viewFor(methods, ReadinessMethodType.SMS).isAdminAddable).toBe(true);
|
|
461
|
+
|
|
462
|
+
expect(viewFor(methods, ReadinessMethodType.Push).isAdminAddable).toBe(
|
|
463
|
+
false,
|
|
464
|
+
);
|
|
465
|
+
expect(viewFor(methods, ReadinessMethodType.Telegram).isAdminAddable).toBe(
|
|
466
|
+
false,
|
|
467
|
+
);
|
|
468
|
+
expect(viewFor(methods, ReadinessMethodType.Webhook).isAdminAddable).toBe(
|
|
469
|
+
false,
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("adding", () => {
|
|
475
|
+
test("writes the row for the TARGET and leaves it unverified", async () => {
|
|
476
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
477
|
+
|
|
478
|
+
await UserNotificationMethodAdminService.addMethodForUser({
|
|
479
|
+
projectId: projectId,
|
|
480
|
+
targetUserId: targetUserId,
|
|
481
|
+
actorUserId: actorUserId,
|
|
482
|
+
methodType: "SMS",
|
|
483
|
+
value: RAW_PHONE,
|
|
484
|
+
props: props(),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const created: UserSMS = smsCreate.mock.calls[0]![0].data as UserSMS;
|
|
488
|
+
|
|
489
|
+
/*
|
|
490
|
+
* The TARGET's id, not the actor's. A row written with the admin's id would
|
|
491
|
+
* be the admin's own method — harmless — but a row written with the
|
|
492
|
+
* target's id and the ADMIN's number is the redirect this whole design
|
|
493
|
+
* exists to prevent, and the two are one typo apart.
|
|
494
|
+
*/
|
|
495
|
+
expect(created.userId!.toString()).toBe(targetUserId.toString());
|
|
496
|
+
expect(created.projectId!.toString()).toBe(projectId.toString());
|
|
497
|
+
expect(created.phone!.toString()).toBe(RAW_PHONE);
|
|
498
|
+
|
|
499
|
+
/*
|
|
500
|
+
* UNVERIFIED, by not being set at all. This is the property everything
|
|
501
|
+
* else rests on: the method service's onCreateSuccess sends a verification
|
|
502
|
+
* code to the number itself precisely because isVerified is falsy, and the
|
|
503
|
+
* verify endpoints then refuse anybody but the row's owner. Setting this
|
|
504
|
+
* true here would make an administrator's typing immediately live.
|
|
505
|
+
*/
|
|
506
|
+
expect(created.isVerified).toBeFalsy();
|
|
507
|
+
|
|
508
|
+
// And nothing about the row is verified afterwards either.
|
|
509
|
+
expect(smsCreate.mock.calls[0]![0].props.isRoot).toBe(true);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("refuses the three channels that cannot be verified by their owner", async () => {
|
|
513
|
+
for (const methodType of [
|
|
514
|
+
"Push",
|
|
515
|
+
"Telegram",
|
|
516
|
+
"Webhook",
|
|
517
|
+
"Carrier Pigeon",
|
|
518
|
+
]) {
|
|
519
|
+
await expect(
|
|
520
|
+
UserNotificationMethodAdminService.addMethodForUser({
|
|
521
|
+
projectId: projectId,
|
|
522
|
+
targetUserId: targetUserId,
|
|
523
|
+
actorUserId: actorUserId,
|
|
524
|
+
methodType: methodType,
|
|
525
|
+
value: "anything",
|
|
526
|
+
props: props(),
|
|
527
|
+
}),
|
|
528
|
+
).rejects.toThrow(BadDataException);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/*
|
|
532
|
+
* One sentence for "no such channel" and for "that channel exists but an
|
|
533
|
+
* administrator may not create it": naming which one it was describes the
|
|
534
|
+
* shape of the feature to somebody probing it.
|
|
535
|
+
*/
|
|
536
|
+
await expect(
|
|
537
|
+
UserNotificationMethodAdminService.addMethodForUser({
|
|
538
|
+
projectId: projectId,
|
|
539
|
+
targetUserId: targetUserId,
|
|
540
|
+
actorUserId: actorUserId,
|
|
541
|
+
methodType: "Webhook",
|
|
542
|
+
value: "https://hooks.example.com/abc",
|
|
543
|
+
props: props(),
|
|
544
|
+
}),
|
|
545
|
+
).rejects.toThrow(/can only add Email, SMS, Call and WhatsApp/);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("validates the value before writing anything", async () => {
|
|
549
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
550
|
+
|
|
551
|
+
await expect(
|
|
552
|
+
UserNotificationMethodAdminService.addMethodForUser({
|
|
553
|
+
projectId: projectId,
|
|
554
|
+
targetUserId: targetUserId,
|
|
555
|
+
actorUserId: actorUserId,
|
|
556
|
+
methodType: "Email",
|
|
557
|
+
value: "not-an-email",
|
|
558
|
+
props: props(),
|
|
559
|
+
}),
|
|
560
|
+
).rejects.toThrow(BadDataException);
|
|
561
|
+
|
|
562
|
+
/*
|
|
563
|
+
* Refused before the row exists rather than stored and discovered later by
|
|
564
|
+
* a page that fails to deliver — which, on this surface, means discovered
|
|
565
|
+
* during an incident.
|
|
566
|
+
*/
|
|
567
|
+
expect(emailCreate).not.toHaveBeenCalled();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("refuses a duplicate rather than creating a second row", async () => {
|
|
571
|
+
await expect(
|
|
572
|
+
UserNotificationMethodAdminService.addMethodForUser({
|
|
573
|
+
projectId: projectId,
|
|
574
|
+
targetUserId: targetUserId,
|
|
575
|
+
actorUserId: actorUserId,
|
|
576
|
+
methodType: "SMS",
|
|
577
|
+
value: RAW_PHONE,
|
|
578
|
+
props: props(),
|
|
579
|
+
}),
|
|
580
|
+
).rejects.toThrow(/already has a SMS notification method/);
|
|
581
|
+
|
|
582
|
+
/*
|
|
583
|
+
* Two identical rows are not merely untidy: they show up as two entries an
|
|
584
|
+
* admin has to tell apart on a masked list, each with its own verification
|
|
585
|
+
* state, and a rule pointed at the unverified one looks correct and pages
|
|
586
|
+
* nobody.
|
|
587
|
+
*/
|
|
588
|
+
expect(smsCreate).not.toHaveBeenCalled();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("refuses a target who is not a member of the project", async () => {
|
|
592
|
+
teamMemberFindOneBy.mockResolvedValue(null as never);
|
|
593
|
+
|
|
594
|
+
await expect(
|
|
595
|
+
UserNotificationMethodAdminService.addMethodForUser({
|
|
596
|
+
projectId: projectId,
|
|
597
|
+
targetUserId: targetUserId,
|
|
598
|
+
actorUserId: actorUserId,
|
|
599
|
+
methodType: "SMS",
|
|
600
|
+
value: RAW_PHONE,
|
|
601
|
+
props: props(),
|
|
602
|
+
}),
|
|
603
|
+
).rejects.toThrow(/not a member of this project/);
|
|
604
|
+
|
|
605
|
+
expect(smsCreate).not.toHaveBeenCalled();
|
|
606
|
+
|
|
607
|
+
/*
|
|
608
|
+
* Membership is read with root props and NO cache.
|
|
609
|
+
* TeamMemberService.getTeamIdsForUser would answer the same question with
|
|
610
|
+
* one fewer query, but it memoises for 60 seconds — and a security decision
|
|
611
|
+
* that keeps saying "yes" for a minute after somebody was removed from the
|
|
612
|
+
* project is not a security decision.
|
|
613
|
+
*/
|
|
614
|
+
const call: any = teamMemberFindOneBy.mock.calls[0]![0];
|
|
615
|
+
|
|
616
|
+
expect(call.query.hasAcceptedInvitation).toBe(true);
|
|
617
|
+
expect(call.props.isRoot).toBe(true);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("tells the owner, naming the actor and the mask", async () => {
|
|
621
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
622
|
+
|
|
623
|
+
await UserNotificationMethodAdminService.addMethodForUser({
|
|
624
|
+
projectId: projectId,
|
|
625
|
+
targetUserId: targetUserId,
|
|
626
|
+
actorUserId: actorUserId,
|
|
627
|
+
methodType: "SMS",
|
|
628
|
+
value: RAW_PHONE,
|
|
629
|
+
props: props(),
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await new Promise<void>((resolve: () => void): void => {
|
|
633
|
+
setTimeout(resolve, 0);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(mailSpy).toHaveBeenCalled();
|
|
637
|
+
|
|
638
|
+
const mail: any = mailSpy.mock.calls[0]![0];
|
|
639
|
+
const recipientOptions: any = mailSpy.mock.calls[0]![1];
|
|
640
|
+
|
|
641
|
+
// To the OWNER, never to the admin who made the change.
|
|
642
|
+
expect(mail.toEmail.toString()).toBe("jane.login@example.com");
|
|
643
|
+
expect(recipientOptions.userId.toString()).toBe(targetUserId.toString());
|
|
644
|
+
|
|
645
|
+
/*
|
|
646
|
+
* The actor is named by email as well as by name: display names are not
|
|
647
|
+
* unique and are user-editable, so "Alex added a phone number" is not
|
|
648
|
+
* something the reader can act on. An address is.
|
|
649
|
+
*/
|
|
650
|
+
expect(mail.vars.message).toContain("Alex Admin");
|
|
651
|
+
expect(mail.vars.message).toContain("alex.admin@example.com");
|
|
652
|
+
|
|
653
|
+
/*
|
|
654
|
+
* And the instruction that makes the mail a control rather than a notice:
|
|
655
|
+
* an addition is inert until the owner verifies it, so "do not verify it"
|
|
656
|
+
* is an action they can take.
|
|
657
|
+
*/
|
|
658
|
+
expect(mail.vars.message).toContain("cannot be used to notify you");
|
|
659
|
+
expect(mail.vars.message).toContain("do not verify it");
|
|
660
|
+
|
|
661
|
+
// The MASK, never the number — this mail crosses an untrusted transport.
|
|
662
|
+
expect(mail.vars.message).toContain(`+1 ${IDENTIFIER_MASK}`);
|
|
663
|
+
expectNothingRawIn(mail.vars);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("does not mail somebody about their own change", async () => {
|
|
667
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
668
|
+
|
|
669
|
+
await UserNotificationMethodAdminService.addMethodForUser({
|
|
670
|
+
projectId: projectId,
|
|
671
|
+
targetUserId: targetUserId,
|
|
672
|
+
actorUserId: targetUserId,
|
|
673
|
+
methodType: "SMS",
|
|
674
|
+
value: RAW_PHONE,
|
|
675
|
+
props: props(),
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await new Promise<void>((resolve: () => void): void => {
|
|
679
|
+
setTimeout(resolve, 0);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// A person who just did the thing does not need to be warned about it.
|
|
683
|
+
expect(mailSpy).not.toHaveBeenCalled();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("drops the readiness cache so the change is visible immediately", async () => {
|
|
687
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
688
|
+
|
|
689
|
+
await UserNotificationMethodAdminService.addMethodForUser({
|
|
690
|
+
projectId: projectId,
|
|
691
|
+
targetUserId: targetUserId,
|
|
692
|
+
actorUserId: actorUserId,
|
|
693
|
+
methodType: "SMS",
|
|
694
|
+
value: RAW_PHONE,
|
|
695
|
+
props: props(),
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
/*
|
|
699
|
+
* The readiness service holds this user's method list for 60 seconds, and
|
|
700
|
+
* the page the admin is looking at re-reads readiness the moment this
|
|
701
|
+
* returns. Left alone it would redraw the responder exactly as they were,
|
|
702
|
+
* which reads as "the add did not work".
|
|
703
|
+
*/
|
|
704
|
+
expect(clearCacheSpy).toHaveBeenCalled();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("returns the created method already masked", async () => {
|
|
708
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
709
|
+
|
|
710
|
+
const created: AdminNotificationMethodView =
|
|
711
|
+
await UserNotificationMethodAdminService.addMethodForUser({
|
|
712
|
+
projectId: projectId,
|
|
713
|
+
targetUserId: targetUserId,
|
|
714
|
+
actorUserId: actorUserId,
|
|
715
|
+
methodType: "SMS",
|
|
716
|
+
value: RAW_PHONE,
|
|
717
|
+
props: props(),
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
expect(created.maskedIdentifier).toBe(
|
|
721
|
+
`+1 ${IDENTIFIER_MASK} ${IDENTIFIER_MASK} 4821`,
|
|
722
|
+
);
|
|
723
|
+
expect(created.isVerified).toBe(false);
|
|
724
|
+
|
|
725
|
+
expectNothingRawIn(created);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
describe("removing", () => {
|
|
730
|
+
test("deletes the row and tells the owner what they lost", async () => {
|
|
731
|
+
await UserNotificationMethodAdminService.deleteMethodForUser({
|
|
732
|
+
projectId: projectId,
|
|
733
|
+
targetUserId: targetUserId,
|
|
734
|
+
actorUserId: actorUserId,
|
|
735
|
+
methodType: "SMS",
|
|
736
|
+
methodId: smsMethodId,
|
|
737
|
+
props: props(),
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
expect(smsDelete).toHaveBeenCalled();
|
|
741
|
+
expect(smsDelete.mock.calls[0]![0].id.toString()).toBe(
|
|
742
|
+
smsMethodId.toString(),
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
await new Promise<void>((resolve: () => void): void => {
|
|
746
|
+
setTimeout(resolve, 0);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const mail: any = mailSpy.mock.calls[0]![0];
|
|
750
|
+
|
|
751
|
+
/*
|
|
752
|
+
* Deletion gets its own sentence rather than the generic one. It is the
|
|
753
|
+
* only one of the two actions that can leave a responder silently
|
|
754
|
+
* unreachable, which is the failure this whole epic exists to stop
|
|
755
|
+
* happening unnoticed.
|
|
756
|
+
*/
|
|
757
|
+
expect(mail.subject).toContain("removed");
|
|
758
|
+
expect(mail.vars.message).toContain("notification rules that used it");
|
|
759
|
+
expect(mail.vars.message).toContain("may no longer be paged");
|
|
760
|
+
|
|
761
|
+
// Still the mask: the owner's own number is not echoed back over SMTP.
|
|
762
|
+
expect(mail.vars.message).toContain(`+1 ${IDENTIFIER_MASK}`);
|
|
763
|
+
expectNothingRawIn(mail.vars);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("refuses a method that belongs to somebody else", async () => {
|
|
767
|
+
/*
|
|
768
|
+
* The row is looked up by (id, projectId, userId) together, so a row
|
|
769
|
+
* belonging to another user simply does not come back — and "not found" and
|
|
770
|
+
* "not yours" are deliberately the same answer, because in both cases the
|
|
771
|
+
* caller named a row they have no business naming.
|
|
772
|
+
*/
|
|
773
|
+
smsFindOneBy.mockResolvedValue(null as never);
|
|
774
|
+
|
|
775
|
+
await expect(
|
|
776
|
+
UserNotificationMethodAdminService.deleteMethodForUser({
|
|
777
|
+
projectId: projectId,
|
|
778
|
+
targetUserId: targetUserId,
|
|
779
|
+
actorUserId: actorUserId,
|
|
780
|
+
methodType: "SMS",
|
|
781
|
+
methodId: smsMethodId,
|
|
782
|
+
props: props(),
|
|
783
|
+
}),
|
|
784
|
+
).rejects.toThrow(/could not be found for this user/);
|
|
785
|
+
|
|
786
|
+
expect(smsDelete).not.toHaveBeenCalled();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("scopes the ownership lookup to the project and the user", async () => {
|
|
790
|
+
await UserNotificationMethodAdminService.deleteMethodForUser({
|
|
791
|
+
projectId: projectId,
|
|
792
|
+
targetUserId: targetUserId,
|
|
793
|
+
actorUserId: actorUserId,
|
|
794
|
+
methodType: "SMS",
|
|
795
|
+
methodId: smsMethodId,
|
|
796
|
+
props: props(),
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const call: any = smsFindOneBy.mock.calls[0]![0];
|
|
800
|
+
|
|
801
|
+
expect(call.query._id.toString()).toBe(smsMethodId.toString());
|
|
802
|
+
expect(call.query.projectId.toString()).toBe(projectId.toString());
|
|
803
|
+
expect(call.query.userId.toString()).toBe(targetUserId.toString());
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("refuses a target who is not a member of the project", async () => {
|
|
807
|
+
teamMemberFindOneBy.mockResolvedValue(null as never);
|
|
808
|
+
|
|
809
|
+
await expect(
|
|
810
|
+
UserNotificationMethodAdminService.deleteMethodForUser({
|
|
811
|
+
projectId: projectId,
|
|
812
|
+
targetUserId: targetUserId,
|
|
813
|
+
actorUserId: actorUserId,
|
|
814
|
+
methodType: "SMS",
|
|
815
|
+
methodId: smsMethodId,
|
|
816
|
+
props: props(),
|
|
817
|
+
}),
|
|
818
|
+
).rejects.toThrow(/not a member of this project/);
|
|
819
|
+
|
|
820
|
+
expect(smsDelete).not.toHaveBeenCalled();
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test("previews the cost without deleting anything", async () => {
|
|
824
|
+
const preview: unknown =
|
|
825
|
+
await UserNotificationMethodAdminService.getDeletionPreview({
|
|
826
|
+
projectId: projectId,
|
|
827
|
+
targetUserId: targetUserId,
|
|
828
|
+
methodType: "SMS",
|
|
829
|
+
methodId: smsMethodId,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
expect(preview).toEqual({
|
|
833
|
+
rulesDeletedCount: 4,
|
|
834
|
+
coverageLostCount: 2,
|
|
835
|
+
verifiedMethodCountAfterDeletion: 0,
|
|
836
|
+
reachability: "NotReachable",
|
|
837
|
+
isFallbackEnabled: true,
|
|
838
|
+
isTruncated: false,
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
expect(smsDelete).not.toHaveBeenCalled();
|
|
842
|
+
|
|
843
|
+
/*
|
|
844
|
+
* The impact is computed by the rules service, which reads EVERY rule for
|
|
845
|
+
* the user rather than a page and derives each rule's severity from the
|
|
846
|
+
* column its rule type dictates. That method was written for this caller
|
|
847
|
+
* and had no production caller until now.
|
|
848
|
+
*/
|
|
849
|
+
const call: any = deletionImpact.mock.calls[0]![0];
|
|
850
|
+
expect(call.methodType).toBe(ReadinessMethodType.SMS);
|
|
851
|
+
expect(call.methodId.toString()).toBe(smsMethodId.toString());
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test("the preview refuses a method that is not this user's", async () => {
|
|
855
|
+
smsFindOneBy.mockResolvedValue(null as never);
|
|
856
|
+
|
|
857
|
+
await expect(
|
|
858
|
+
UserNotificationMethodAdminService.getDeletionPreview({
|
|
859
|
+
projectId: projectId,
|
|
860
|
+
targetUserId: targetUserId,
|
|
861
|
+
methodType: "SMS",
|
|
862
|
+
methodId: smsMethodId,
|
|
863
|
+
}),
|
|
864
|
+
).rejects.toThrow(/could not be found for this user/);
|
|
865
|
+
|
|
866
|
+
/*
|
|
867
|
+
* Otherwise the preview is a probe: it would report rule counts for a
|
|
868
|
+
* method id belonging to another user or another project.
|
|
869
|
+
*/
|
|
870
|
+
expect(deletionImpact).not.toHaveBeenCalled();
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
describe("resending a verification code", () => {
|
|
875
|
+
test("asks the channel's own service to send it again", async () => {
|
|
876
|
+
await UserNotificationMethodAdminService.resendVerificationCodeForUser({
|
|
877
|
+
projectId: projectId,
|
|
878
|
+
targetUserId: targetUserId,
|
|
879
|
+
methodType: "SMS",
|
|
880
|
+
methodId: smsMethodId,
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
expect(smsResend).toHaveBeenCalledWith(smsMethodId);
|
|
884
|
+
|
|
885
|
+
/*
|
|
886
|
+
* It discloses nothing: the code goes to the number on the row, which is
|
|
887
|
+
* the one thing on it the administrator cannot read. That is what makes
|
|
888
|
+
* this the useful lever after adding a method — "I added your work mobile,
|
|
889
|
+
* here is another code" rather than re-typing the number.
|
|
890
|
+
*/
|
|
891
|
+
expect(mailSpy).not.toHaveBeenCalled();
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test("refuses a channel with no verification concept", async () => {
|
|
895
|
+
jest
|
|
896
|
+
.spyOn(UserWebhookService, "findOneBy")
|
|
897
|
+
.mockResolvedValue(buildWebhookRow() as never);
|
|
898
|
+
|
|
899
|
+
await expect(
|
|
900
|
+
UserNotificationMethodAdminService.resendVerificationCodeForUser({
|
|
901
|
+
projectId: projectId,
|
|
902
|
+
targetUserId: targetUserId,
|
|
903
|
+
methodType: "Webhook",
|
|
904
|
+
methodId: ObjectID.generate(),
|
|
905
|
+
}),
|
|
906
|
+
).rejects.toThrow(/do not use a verification code/);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test("refuses a method that is not this user's", async () => {
|
|
910
|
+
smsFindOneBy.mockResolvedValue(null as never);
|
|
911
|
+
|
|
912
|
+
await expect(
|
|
913
|
+
UserNotificationMethodAdminService.resendVerificationCodeForUser({
|
|
914
|
+
projectId: projectId,
|
|
915
|
+
targetUserId: targetUserId,
|
|
916
|
+
methodType: "SMS",
|
|
917
|
+
methodId: smsMethodId,
|
|
918
|
+
}),
|
|
919
|
+
).rejects.toThrow(/could not be found for this user/);
|
|
920
|
+
|
|
921
|
+
expect(smsResend).not.toHaveBeenCalled();
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
describe("the mail cannot break the write", () => {
|
|
926
|
+
test("a failed notification leaves the method in place", async () => {
|
|
927
|
+
smsFindBy.mockResolvedValue([] as never);
|
|
928
|
+
mailSpy.mockRejectedValue(new Error("smtp is down") as never);
|
|
929
|
+
|
|
930
|
+
const created: AdminNotificationMethodView =
|
|
931
|
+
await UserNotificationMethodAdminService.addMethodForUser({
|
|
932
|
+
projectId: projectId,
|
|
933
|
+
targetUserId: targetUserId,
|
|
934
|
+
actorUserId: actorUserId,
|
|
935
|
+
methodType: "SMS",
|
|
936
|
+
value: RAW_PHONE,
|
|
937
|
+
props: props(),
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
/*
|
|
941
|
+
* The row is already committed by the time the mail is attempted, so
|
|
942
|
+
* throwing then would report a failure for a change that happened — and the
|
|
943
|
+
* caller would be entitled to believe it did not, and to try again.
|
|
944
|
+
*/
|
|
945
|
+
expect(created.methodId).toBeDefined();
|
|
946
|
+
expect(smsCreate).toHaveBeenCalled();
|
|
947
|
+
});
|
|
948
|
+
});
|