@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,963 @@
|
|
|
1
|
+
import { mockRouter } from "./Helpers";
|
|
2
|
+
import UserNotificationMethodAdminAPI from "../../../Server/API/UserNotificationMethodAdminAPI";
|
|
3
|
+
import CommonAPI from "../../../Server/API/CommonAPI";
|
|
4
|
+
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
5
|
+
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
6
|
+
import UserNotificationMethodAdminService, {
|
|
7
|
+
AdminNotificationMethodView,
|
|
8
|
+
} from "../../../Server/Services/UserNotificationMethodAdminService";
|
|
9
|
+
import { ReadinessMethodType } from "../../../Server/Services/OnCallReadinessService";
|
|
10
|
+
import {
|
|
11
|
+
ExpressRequest,
|
|
12
|
+
ExpressResponse,
|
|
13
|
+
NextFunction,
|
|
14
|
+
} from "../../../Server/Utils/Express";
|
|
15
|
+
import Response from "../../../Server/Utils/Response";
|
|
16
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
17
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
18
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
19
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
20
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
21
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
22
|
+
import Permission, {
|
|
23
|
+
UserPermission,
|
|
24
|
+
UserTenantAccessPermission,
|
|
25
|
+
} from "../../../Types/Permission";
|
|
26
|
+
import { beforeEach, describe, expect, test } from "@jest/globals";
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* The HTTP face of "an administrator manages somebody else's notification
|
|
30
|
+
* methods".
|
|
31
|
+
*
|
|
32
|
+
* This router is the ONLY gate under these routes, and that is not a stylistic
|
|
33
|
+
* observation. The seven notification method models are scoped to the person
|
|
34
|
+
* who owns the device — the essay at the top of UserEmail.ts is the record of
|
|
35
|
+
* what happened the one time that scope was widened — so the service behind
|
|
36
|
+
* these handlers does every read and write with `isRoot: true`, which means the
|
|
37
|
+
* models' own TableAccessControl never runs on this path. And
|
|
38
|
+
* UserMiddleware.getUserMiddleware is not a gate either: it admits anonymous
|
|
39
|
+
* callers as UserType.Public and calls next(), and the tenant id it attaches
|
|
40
|
+
* comes straight off the caller-supplied `tenantid` header before any
|
|
41
|
+
* authorisation runs.
|
|
42
|
+
*
|
|
43
|
+
* So a disproportionate share of this file is about who is refused, in what
|
|
44
|
+
* order, and — the property that stops these endpoints being a cross-tenant
|
|
45
|
+
* probe — whether a caller can tell the refusals apart.
|
|
46
|
+
*
|
|
47
|
+
* The service itself is stubbed here. What it does with the row (mask it, leave
|
|
48
|
+
* it unverified, mail the owner) is a different claim tested against the real
|
|
49
|
+
* thing in UserNotificationMethodAdminService.test.ts; a masking test that
|
|
50
|
+
* stubbed the masker would prove nothing.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
54
|
+
return {
|
|
55
|
+
getRouter: () => {
|
|
56
|
+
return mockRouter;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
62
|
+
return {
|
|
63
|
+
sendJsonObjectResponse: jest.fn().mockImplementation((...args: []) => {
|
|
64
|
+
return args;
|
|
65
|
+
}),
|
|
66
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
67
|
+
sendErrorResponse: jest.fn().mockImplementation((...args: []) => {
|
|
68
|
+
return args;
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const LIST_ROUTE: string = "/user-notification-method-admin/user/:userId";
|
|
74
|
+
const ADD_ROUTE: string = "/user-notification-method-admin/user/:userId";
|
|
75
|
+
const IMPACT_ROUTE: string =
|
|
76
|
+
"/user-notification-method-admin/user/:userId/:methodType/:methodId/deletion-impact";
|
|
77
|
+
const RESEND_ROUTE: string =
|
|
78
|
+
"/user-notification-method-admin/user/:userId/:methodType/:methodId/resend-verification-code";
|
|
79
|
+
const DELETE_ROUTE: string =
|
|
80
|
+
"/user-notification-method-admin/user/:userId/:methodType/:methodId";
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* The one sentence every authorisation refusal on this router carries.
|
|
84
|
+
* Asserting on the literal is the only way to pin the property that matters:
|
|
85
|
+
* "you may not do this", "that user is in another project" and "that user does
|
|
86
|
+
* not exist" must be indistinguishable to the caller, or the endpoint becomes a
|
|
87
|
+
* way to enumerate the installation's users.
|
|
88
|
+
*/
|
|
89
|
+
const REFUSAL: string = "You are not authorized to access this project's data.";
|
|
90
|
+
|
|
91
|
+
interface RegisteredRoute {
|
|
92
|
+
method: string;
|
|
93
|
+
uri: string;
|
|
94
|
+
middleware: unknown;
|
|
95
|
+
middlewares: Array<unknown>;
|
|
96
|
+
handlerFunction: (
|
|
97
|
+
req: ExpressRequest,
|
|
98
|
+
res: ExpressResponse,
|
|
99
|
+
next: NextFunction,
|
|
100
|
+
) => void | Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface RouteCallResult {
|
|
104
|
+
thrownToNext: unknown;
|
|
105
|
+
nextCallCount: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let propsSpy: jest.SpyInstance;
|
|
109
|
+
let teamMemberFindBy: jest.SpyInstance;
|
|
110
|
+
let listSpy: jest.SpyInstance;
|
|
111
|
+
let addSpy: jest.SpyInstance;
|
|
112
|
+
let deleteSpy: jest.SpyInstance;
|
|
113
|
+
let resendSpy: jest.SpyInstance;
|
|
114
|
+
let previewSpy: jest.SpyInstance;
|
|
115
|
+
|
|
116
|
+
let projectId: ObjectID;
|
|
117
|
+
let otherProjectId: ObjectID;
|
|
118
|
+
let callerUserId: ObjectID;
|
|
119
|
+
let targetUserId: ObjectID;
|
|
120
|
+
let methodId: ObjectID;
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* A logged-in caller holding exactly the permissions named, in the project
|
|
124
|
+
* named.
|
|
125
|
+
*
|
|
126
|
+
* `isBlockPermission` is set explicitly on every entry because the dictionary
|
|
127
|
+
* these live in holds GRANTS AND DENIALS together, discriminated only by that
|
|
128
|
+
* flag. A fixture that left it undefined would be a grant by accident, and the
|
|
129
|
+
* denial test below would then be testing nothing.
|
|
130
|
+
*/
|
|
131
|
+
function buildProps(data: {
|
|
132
|
+
projectId: ObjectID;
|
|
133
|
+
userId: ObjectID;
|
|
134
|
+
permissions: Array<Permission>;
|
|
135
|
+
blockedPermissions?: Array<Permission>;
|
|
136
|
+
}): DatabaseCommonInteractionProps {
|
|
137
|
+
const granted: Array<UserPermission> = data.permissions.map(
|
|
138
|
+
(permission: Permission): UserPermission => {
|
|
139
|
+
return {
|
|
140
|
+
_type: "UserPermission",
|
|
141
|
+
permission: permission,
|
|
142
|
+
labelIds: [],
|
|
143
|
+
isBlockPermission: false,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const blocked: Array<UserPermission> = (data.blockedPermissions || []).map(
|
|
149
|
+
(permission: Permission): UserPermission => {
|
|
150
|
+
return {
|
|
151
|
+
_type: "UserPermission",
|
|
152
|
+
permission: permission,
|
|
153
|
+
labelIds: [],
|
|
154
|
+
isBlockPermission: true,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
160
|
+
_type: "UserTenantAccessPermission",
|
|
161
|
+
projectId: data.projectId,
|
|
162
|
+
permissions: [...granted, ...blocked],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const permissionMap: Dictionary<UserTenantAccessPermission> = {};
|
|
166
|
+
permissionMap[data.projectId.toString()] = tenantPermission;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
tenantId: data.projectId,
|
|
170
|
+
userId: data.userId,
|
|
171
|
+
userTenantAccessPermission: permissionMap,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function routeFor(method: string, uri: string): RegisteredRoute {
|
|
176
|
+
const routes: Array<RegisteredRoute> =
|
|
177
|
+
mockRouter.routes as unknown as Array<RegisteredRoute>;
|
|
178
|
+
|
|
179
|
+
const route: RegisteredRoute | undefined = routes.find(
|
|
180
|
+
(candidate: RegisteredRoute): boolean => {
|
|
181
|
+
return candidate.method === method && candidate.uri === uri;
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (!route) {
|
|
186
|
+
throw new Error(`No ${method} route registered for ${uri}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return route;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function callRoute(data: {
|
|
193
|
+
method: string;
|
|
194
|
+
uri: string;
|
|
195
|
+
params?: Dictionary<string> | undefined;
|
|
196
|
+
body?: JSONObject | undefined;
|
|
197
|
+
}): Promise<RouteCallResult> {
|
|
198
|
+
const req: ExpressRequest = {
|
|
199
|
+
params: data.params || {},
|
|
200
|
+
query: {},
|
|
201
|
+
body: data.body,
|
|
202
|
+
headers: {},
|
|
203
|
+
} as unknown as ExpressRequest;
|
|
204
|
+
|
|
205
|
+
const res: ExpressResponse = {
|
|
206
|
+
send: jest.fn(),
|
|
207
|
+
json: jest.fn(),
|
|
208
|
+
status: jest.fn().mockReturnThis(),
|
|
209
|
+
} as unknown as ExpressResponse;
|
|
210
|
+
|
|
211
|
+
const next: jest.Mock = jest.fn();
|
|
212
|
+
|
|
213
|
+
await routeFor(data.method, data.uri).handlerFunction(
|
|
214
|
+
req,
|
|
215
|
+
res,
|
|
216
|
+
next as unknown as NextFunction,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
thrownToNext: next.mock.calls[0] ? next.mock.calls[0][0] : undefined,
|
|
221
|
+
nextCallCount: next.mock.calls.length,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
type StubCallerFunction = (permissions: Array<Permission>) => void;
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* The mutable module-level spies and ids are reached through functions rather
|
|
229
|
+
* than captured directly, because the parameterised blocks below build their
|
|
230
|
+
* callbacks inside a `for` loop: a closure over `propsSpy` would read whichever
|
|
231
|
+
* spy happened to be bound when the loop ran rather than the one this test's
|
|
232
|
+
* beforeEach created. Going through a stable function is also what stops
|
|
233
|
+
* eslint's no-loop-func from being right about it.
|
|
234
|
+
*/
|
|
235
|
+
const stubCaller: StubCallerFunction = (
|
|
236
|
+
permissions: Array<Permission>,
|
|
237
|
+
): void => {
|
|
238
|
+
propsSpy.mockResolvedValue(
|
|
239
|
+
buildProps({
|
|
240
|
+
projectId: projectId,
|
|
241
|
+
userId: callerUserId,
|
|
242
|
+
permissions: permissions,
|
|
243
|
+
}) as never,
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
type StubTargetOutsideProjectFunction = () => void;
|
|
248
|
+
|
|
249
|
+
const stubTargetOutsideProject: StubTargetOutsideProjectFunction = (): void => {
|
|
250
|
+
teamMemberFindBy.mockResolvedValue([] as never);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function jsonPayload(): Record<string, unknown> {
|
|
254
|
+
const sender: jest.Mock =
|
|
255
|
+
Response.sendJsonObjectResponse as unknown as jest.Mock;
|
|
256
|
+
const calls: Array<Array<unknown>> = sender.mock.calls;
|
|
257
|
+
const last: Array<unknown> | undefined = calls[calls.length - 1];
|
|
258
|
+
|
|
259
|
+
if (!last) {
|
|
260
|
+
throw new Error("The route sent no JSON response");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return last[2] as Record<string, unknown>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Enough of a view to serialise, carrying no raw value anywhere. */
|
|
267
|
+
function buildView(): AdminNotificationMethodView {
|
|
268
|
+
return {
|
|
269
|
+
methodId: methodId,
|
|
270
|
+
methodType: ReadinessMethodType.SMS,
|
|
271
|
+
maskedIdentifier: "+1 ••• ••• 4821",
|
|
272
|
+
isVerified: false,
|
|
273
|
+
isAdminAddable: true,
|
|
274
|
+
createdAt: new Date("2024-01-02T03:04:05.000Z"),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function everyWriteSpy(): Array<jest.SpyInstance> {
|
|
279
|
+
return [addSpy, deleteSpy, resendSpy, previewSpy];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
beforeEach(() => {
|
|
283
|
+
jest.clearAllMocks();
|
|
284
|
+
|
|
285
|
+
projectId = ObjectID.generate();
|
|
286
|
+
otherProjectId = ObjectID.generate();
|
|
287
|
+
callerUserId = ObjectID.generate();
|
|
288
|
+
targetUserId = ObjectID.generate();
|
|
289
|
+
methodId = ObjectID.generate();
|
|
290
|
+
|
|
291
|
+
propsSpy = jest
|
|
292
|
+
.spyOn(CommonAPI, "getDatabaseCommonInteractionProps")
|
|
293
|
+
.mockResolvedValue(
|
|
294
|
+
buildProps({
|
|
295
|
+
projectId: projectId,
|
|
296
|
+
userId: callerUserId,
|
|
297
|
+
permissions: [
|
|
298
|
+
Permission.ProjectMember,
|
|
299
|
+
Permission.ManageProjectUserNotificationMethod,
|
|
300
|
+
Permission.ReadProjectUserNotificationRule,
|
|
301
|
+
],
|
|
302
|
+
}) as never,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
teamMemberFindBy = jest
|
|
306
|
+
.spyOn(TeamMemberService, "findBy")
|
|
307
|
+
.mockResolvedValue([{ _id: ObjectID.generate().toString() }] as never);
|
|
308
|
+
|
|
309
|
+
listSpy = jest
|
|
310
|
+
.spyOn(UserNotificationMethodAdminService, "listMethodsForUser")
|
|
311
|
+
.mockResolvedValue([buildView()] as never);
|
|
312
|
+
|
|
313
|
+
addSpy = jest
|
|
314
|
+
.spyOn(UserNotificationMethodAdminService, "addMethodForUser")
|
|
315
|
+
.mockResolvedValue(buildView() as never);
|
|
316
|
+
|
|
317
|
+
deleteSpy = jest
|
|
318
|
+
.spyOn(UserNotificationMethodAdminService, "deleteMethodForUser")
|
|
319
|
+
.mockResolvedValue(undefined as never);
|
|
320
|
+
|
|
321
|
+
resendSpy = jest
|
|
322
|
+
.spyOn(UserNotificationMethodAdminService, "resendVerificationCodeForUser")
|
|
323
|
+
.mockResolvedValue(undefined as never);
|
|
324
|
+
|
|
325
|
+
previewSpy = jest
|
|
326
|
+
.spyOn(UserNotificationMethodAdminService, "getDeletionPreview")
|
|
327
|
+
.mockResolvedValue({
|
|
328
|
+
rulesDeletedCount: 2,
|
|
329
|
+
coverageLostCount: 1,
|
|
330
|
+
verifiedMethodCountAfterDeletion: 0,
|
|
331
|
+
reachability: "NotReachable",
|
|
332
|
+
isFallbackEnabled: true,
|
|
333
|
+
isTruncated: false,
|
|
334
|
+
} as never);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("the routes it registers", () => {
|
|
338
|
+
test("five routes, each behind the user middleware", () => {
|
|
339
|
+
const expected: Array<{ method: string; uri: string }> = [
|
|
340
|
+
{ method: "GET", uri: LIST_ROUTE },
|
|
341
|
+
{ method: "POST", uri: ADD_ROUTE },
|
|
342
|
+
{ method: "GET", uri: IMPACT_ROUTE },
|
|
343
|
+
{ method: "POST", uri: RESEND_ROUTE },
|
|
344
|
+
{ method: "DELETE", uri: DELETE_ROUTE },
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
for (const route of expected) {
|
|
348
|
+
const registered: RegisteredRoute = routeFor(route.method, route.uri);
|
|
349
|
+
|
|
350
|
+
/*
|
|
351
|
+
* The middleware does not authorise anything — it admits anonymous
|
|
352
|
+
* callers — but its absence would mean the handler runs with no resolved
|
|
353
|
+
* session at all, and `props.userId` would be undefined on a route that
|
|
354
|
+
* names the actor in an audit entry and in a mail to the account holder.
|
|
355
|
+
*/
|
|
356
|
+
expect(registered.middleware).toBe(UserMiddleware.getUserMiddleware);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("the router is exported as a bare router, not a CRUD API", () => {
|
|
361
|
+
/*
|
|
362
|
+
* Extending BaseAPI to inherit `this.router` would register a whole model's
|
|
363
|
+
* CRUD routes as a side effect — dead code that is shadowed by the real
|
|
364
|
+
* registration until the day the mount order changes. There is no model to
|
|
365
|
+
* inherit here in any case: the seven method models stay owner-scoped and
|
|
366
|
+
* this router deliberately never exposes them.
|
|
367
|
+
*/
|
|
368
|
+
expect(UserNotificationMethodAdminAPI).toBe(mockRouter);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("reading the masked list", () => {
|
|
373
|
+
test("returns one masked entry per method and no raw field", async () => {
|
|
374
|
+
const result: RouteCallResult = await callRoute({
|
|
375
|
+
method: "GET",
|
|
376
|
+
uri: LIST_ROUTE,
|
|
377
|
+
params: { userId: targetUserId.toString() },
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(result.nextCallCount).toBe(0);
|
|
381
|
+
|
|
382
|
+
const payload: Record<string, unknown> = jsonPayload();
|
|
383
|
+
const methods: Array<Record<string, unknown>> = payload["methods"] as Array<
|
|
384
|
+
Record<string, unknown>
|
|
385
|
+
>;
|
|
386
|
+
|
|
387
|
+
expect(methods).toHaveLength(1);
|
|
388
|
+
|
|
389
|
+
/*
|
|
390
|
+
* Ids are flattened to plain strings rather than left as ObjectID's
|
|
391
|
+
* `{ _type, value }` JSON envelope, which is right for model payloads the
|
|
392
|
+
* client parses back but wrong for a hand-rolled body.
|
|
393
|
+
*/
|
|
394
|
+
expect(methods[0]!["methodId"]).toBe(methodId.toString());
|
|
395
|
+
expect(methods[0]!["methodType"]).toBe("SMS");
|
|
396
|
+
expect(methods[0]!["maskedIdentifier"]).toBe("+1 ••• ••• 4821");
|
|
397
|
+
|
|
398
|
+
/*
|
|
399
|
+
* The serialiser copies the fields it names and nothing else. There is no
|
|
400
|
+
* raw field on the service's contract to leak, which is the point — a
|
|
401
|
+
* future change would have to widen that contract in the open rather than
|
|
402
|
+
* slip a phone number through here.
|
|
403
|
+
*/
|
|
404
|
+
expect(Object.keys(methods[0]!).sort()).toEqual(
|
|
405
|
+
[
|
|
406
|
+
"createdAt",
|
|
407
|
+
"isAdminAddable",
|
|
408
|
+
"isVerified",
|
|
409
|
+
"maskedIdentifier",
|
|
410
|
+
"methodId",
|
|
411
|
+
"methodType",
|
|
412
|
+
].sort(),
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("a read-only admin permission is enough to look", async () => {
|
|
417
|
+
propsSpy.mockResolvedValue(
|
|
418
|
+
buildProps({
|
|
419
|
+
projectId: projectId,
|
|
420
|
+
userId: callerUserId,
|
|
421
|
+
permissions: [
|
|
422
|
+
Permission.ProjectMember,
|
|
423
|
+
Permission.ReadProjectUserNotificationRule,
|
|
424
|
+
],
|
|
425
|
+
}) as never,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const result: RouteCallResult = await callRoute({
|
|
429
|
+
method: "GET",
|
|
430
|
+
uri: LIST_ROUTE,
|
|
431
|
+
params: { userId: targetUserId.toString() },
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(result.nextCallCount).toBe(0);
|
|
435
|
+
expect(listSpy).toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("an ordinary member may not read a colleague's methods", async () => {
|
|
439
|
+
propsSpy.mockResolvedValue(
|
|
440
|
+
buildProps({
|
|
441
|
+
projectId: projectId,
|
|
442
|
+
userId: callerUserId,
|
|
443
|
+
permissions: [Permission.ProjectMember],
|
|
444
|
+
}) as never,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const result: RouteCallResult = await callRoute({
|
|
448
|
+
method: "GET",
|
|
449
|
+
uri: LIST_ROUTE,
|
|
450
|
+
params: { userId: targetUserId.toString() },
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
454
|
+
expect((result.thrownToNext as NotAuthorizedException).message).toBe(
|
|
455
|
+
REFUSAL,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
/*
|
|
459
|
+
* Refused BEFORE the read. A route that fetches and then refuses has
|
|
460
|
+
* already paid for — and, on a replica with logging, recorded — the
|
|
461
|
+
* disclosure it declined to make.
|
|
462
|
+
*/
|
|
463
|
+
expect(listSpy).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("but that same member may read their own", async () => {
|
|
467
|
+
propsSpy.mockResolvedValue(
|
|
468
|
+
buildProps({
|
|
469
|
+
projectId: projectId,
|
|
470
|
+
userId: targetUserId,
|
|
471
|
+
permissions: [Permission.ProjectMember],
|
|
472
|
+
}) as never,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const result: RouteCallResult = await callRoute({
|
|
476
|
+
method: "GET",
|
|
477
|
+
uri: LIST_ROUTE,
|
|
478
|
+
params: { userId: targetUserId.toString() },
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
/*
|
|
482
|
+
* These are rows the self-serve settings pages already show them in full,
|
|
483
|
+
* so refusing a masked view of their own account would be theatre.
|
|
484
|
+
*/
|
|
485
|
+
expect(result.nextCallCount).toBe(0);
|
|
486
|
+
expect(listSpy).toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("who may write", () => {
|
|
491
|
+
/*
|
|
492
|
+
* The four write routes, described lazily.
|
|
493
|
+
*
|
|
494
|
+
* `getParams` and `getBody` are functions rather than values because the ids
|
|
495
|
+
* every one of them names are regenerated in beforeEach, and a describe body
|
|
496
|
+
* runs once at collection time — before the first beforeEach has assigned
|
|
497
|
+
* anything. Building the params eagerly reads `undefined.toString()` and
|
|
498
|
+
* takes the whole file down at load.
|
|
499
|
+
*/
|
|
500
|
+
interface WriteCase {
|
|
501
|
+
label: string;
|
|
502
|
+
method: string;
|
|
503
|
+
uri: string;
|
|
504
|
+
getParams: () => Dictionary<string>;
|
|
505
|
+
getBody: () => JSONObject | undefined;
|
|
506
|
+
spy: () => jest.SpyInstance;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const WRITE_CASES: Array<WriteCase> = [
|
|
510
|
+
{
|
|
511
|
+
label: "add",
|
|
512
|
+
method: "POST",
|
|
513
|
+
uri: ADD_ROUTE,
|
|
514
|
+
getParams: (): Dictionary<string> => {
|
|
515
|
+
return { userId: targetUserId.toString() };
|
|
516
|
+
},
|
|
517
|
+
getBody: (): JSONObject => {
|
|
518
|
+
return { methodType: "SMS", value: "+15551234821" };
|
|
519
|
+
},
|
|
520
|
+
spy: (): jest.SpyInstance => {
|
|
521
|
+
return addSpy;
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
label: "remove",
|
|
526
|
+
method: "DELETE",
|
|
527
|
+
uri: DELETE_ROUTE,
|
|
528
|
+
getParams: (): Dictionary<string> => {
|
|
529
|
+
return {
|
|
530
|
+
userId: targetUserId.toString(),
|
|
531
|
+
methodType: "SMS",
|
|
532
|
+
methodId: methodId.toString(),
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
getBody: (): undefined => {
|
|
536
|
+
return undefined;
|
|
537
|
+
},
|
|
538
|
+
spy: (): jest.SpyInstance => {
|
|
539
|
+
return deleteSpy;
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
label: "resend the verification code",
|
|
544
|
+
method: "POST",
|
|
545
|
+
uri: RESEND_ROUTE,
|
|
546
|
+
getParams: (): Dictionary<string> => {
|
|
547
|
+
return {
|
|
548
|
+
userId: targetUserId.toString(),
|
|
549
|
+
methodType: "SMS",
|
|
550
|
+
methodId: methodId.toString(),
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
getBody: (): JSONObject => {
|
|
554
|
+
return {};
|
|
555
|
+
},
|
|
556
|
+
spy: (): jest.SpyInstance => {
|
|
557
|
+
return resendSpy;
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
label: "preview a removal",
|
|
562
|
+
method: "GET",
|
|
563
|
+
uri: IMPACT_ROUTE,
|
|
564
|
+
getParams: (): Dictionary<string> => {
|
|
565
|
+
return {
|
|
566
|
+
userId: targetUserId.toString(),
|
|
567
|
+
methodType: "SMS",
|
|
568
|
+
methodId: methodId.toString(),
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
getBody: (): undefined => {
|
|
572
|
+
return undefined;
|
|
573
|
+
},
|
|
574
|
+
spy: (): jest.SpyInstance => {
|
|
575
|
+
return previewSpy;
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
for (const writeCase of WRITE_CASES) {
|
|
581
|
+
test(`${writeCase.label} is refused for a caller who may only READ rules`, async () => {
|
|
582
|
+
/*
|
|
583
|
+
* Including the rule EDIT permission, which is the whole point of the
|
|
584
|
+
* separation: repairing somebody's rules re-points their pages between
|
|
585
|
+
* devices they have already proved they hold; adding a method introduces
|
|
586
|
+
* a device nobody has proved anything about.
|
|
587
|
+
*/
|
|
588
|
+
stubCaller([
|
|
589
|
+
Permission.ProjectMember,
|
|
590
|
+
Permission.ReadProjectUserNotificationRule,
|
|
591
|
+
Permission.EditProjectUserNotificationRule,
|
|
592
|
+
]);
|
|
593
|
+
|
|
594
|
+
const result: RouteCallResult = await callRoute({
|
|
595
|
+
method: writeCase.method,
|
|
596
|
+
uri: writeCase.uri,
|
|
597
|
+
params: writeCase.getParams(),
|
|
598
|
+
body: writeCase.getBody(),
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
602
|
+
expect((result.thrownToNext as NotAuthorizedException).message).toBe(
|
|
603
|
+
REFUSAL,
|
|
604
|
+
);
|
|
605
|
+
expect(writeCase.spy()).not.toHaveBeenCalled();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test(`${writeCase.label} is allowed for a project owner`, async () => {
|
|
609
|
+
/*
|
|
610
|
+
* And with NO granular permission. Existing project teams are seeded with
|
|
611
|
+
* roles, never with individual granular permissions, so a gate naming
|
|
612
|
+
* only ManageProjectUserNotificationMethod would be dead on arrival for
|
|
613
|
+
* every project that already exists.
|
|
614
|
+
*/
|
|
615
|
+
stubCaller([Permission.ProjectOwner]);
|
|
616
|
+
|
|
617
|
+
const result: RouteCallResult = await callRoute({
|
|
618
|
+
method: writeCase.method,
|
|
619
|
+
uri: writeCase.uri,
|
|
620
|
+
params: writeCase.getParams(),
|
|
621
|
+
body: writeCase.getBody(),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
expect(result.nextCallCount).toBe(0);
|
|
625
|
+
expect(writeCase.spy()).toHaveBeenCalled();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test(`${writeCase.label} is refused when the target is not in the project`, async () => {
|
|
629
|
+
stubTargetOutsideProject();
|
|
630
|
+
|
|
631
|
+
const result: RouteCallResult = await callRoute({
|
|
632
|
+
method: writeCase.method,
|
|
633
|
+
uri: writeCase.uri,
|
|
634
|
+
params: writeCase.getParams(),
|
|
635
|
+
body: writeCase.getBody(),
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
/*
|
|
639
|
+
* Holding an administrative permission is a claim about a PROJECT, so it
|
|
640
|
+
* can only ever authorise acting on users of that project. Without this,
|
|
641
|
+
* one throwaway project where the caller is an admin would license adding
|
|
642
|
+
* a phone number to any user id in the installation.
|
|
643
|
+
*
|
|
644
|
+
* The refusal is word-for-word the permission refusal, so "that user is
|
|
645
|
+
* in another project" and "that user does not exist" cannot be told
|
|
646
|
+
* apart.
|
|
647
|
+
*/
|
|
648
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
649
|
+
expect((result.thrownToNext as NotAuthorizedException).message).toBe(
|
|
650
|
+
REFUSAL,
|
|
651
|
+
);
|
|
652
|
+
expect(writeCase.spy()).not.toHaveBeenCalled();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test(`${writeCase.label} rejects a user id that is not a uuid`, async () => {
|
|
656
|
+
const result: RouteCallResult = await callRoute({
|
|
657
|
+
method: writeCase.method,
|
|
658
|
+
uri: writeCase.uri,
|
|
659
|
+
params: { ...writeCase.getParams(), userId: "not-a-uuid" },
|
|
660
|
+
body: writeCase.getBody(),
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
/*
|
|
664
|
+
* ObjectID's constructor accepts any string, so an unvalidated segment
|
|
665
|
+
* would travel to a query that matches nothing and read as "this user has
|
|
666
|
+
* no methods" rather than as "you sent nonsense".
|
|
667
|
+
*/
|
|
668
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
669
|
+
expect(writeCase.spy()).not.toHaveBeenCalled();
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
test("a blocked ProjectAdmin is a denial, never a grant", async () => {
|
|
674
|
+
propsSpy.mockResolvedValue(
|
|
675
|
+
buildProps({
|
|
676
|
+
projectId: projectId,
|
|
677
|
+
userId: callerUserId,
|
|
678
|
+
permissions: [Permission.ProjectMember],
|
|
679
|
+
blockedPermissions: [
|
|
680
|
+
Permission.ProjectAdmin,
|
|
681
|
+
Permission.ManageProjectUserNotificationMethod,
|
|
682
|
+
],
|
|
683
|
+
}) as never,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
const result: RouteCallResult = await callRoute({
|
|
687
|
+
method: "POST",
|
|
688
|
+
uri: ADD_ROUTE,
|
|
689
|
+
params: { userId: targetUserId.toString() },
|
|
690
|
+
body: { methodType: "Email", value: "someone@example.com" },
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
/*
|
|
694
|
+
* The permission dictionary holds grants and denials in one array,
|
|
695
|
+
* discriminated only by `isBlockPermission`. Reading it raw would count a
|
|
696
|
+
* team's explicit BLOCK of ProjectAdmin as a grant of it — so the admin
|
|
697
|
+
* action that RESTRICTS a team would be the thing that handed it this
|
|
698
|
+
* endpoint.
|
|
699
|
+
*/
|
|
700
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
701
|
+
expect(addSpy).not.toHaveBeenCalled();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("a master admin is let through", async () => {
|
|
705
|
+
propsSpy.mockResolvedValue({
|
|
706
|
+
...buildProps({
|
|
707
|
+
projectId: projectId,
|
|
708
|
+
userId: callerUserId,
|
|
709
|
+
permissions: [],
|
|
710
|
+
}),
|
|
711
|
+
isMasterAdmin: true,
|
|
712
|
+
} as never);
|
|
713
|
+
|
|
714
|
+
const result: RouteCallResult = await callRoute({
|
|
715
|
+
method: "POST",
|
|
716
|
+
uri: ADD_ROUTE,
|
|
717
|
+
params: { userId: targetUserId.toString() },
|
|
718
|
+
body: { methodType: "Email", value: "someone@example.com" },
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
expect(result.nextCallCount).toBe(0);
|
|
722
|
+
expect(addSpy).toHaveBeenCalled();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("being the owner of the account is NOT a licence to use this router", async () => {
|
|
726
|
+
propsSpy.mockResolvedValue(
|
|
727
|
+
buildProps({
|
|
728
|
+
projectId: projectId,
|
|
729
|
+
userId: targetUserId,
|
|
730
|
+
permissions: [Permission.ProjectMember],
|
|
731
|
+
}) as never,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
const result: RouteCallResult = await callRoute({
|
|
735
|
+
method: "POST",
|
|
736
|
+
uri: ADD_ROUTE,
|
|
737
|
+
params: { userId: targetUserId.toString() },
|
|
738
|
+
body: { methodType: "Email", value: "someone@example.com" },
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
/*
|
|
742
|
+
* The self branch exists on the READ and deliberately not on the writes. A
|
|
743
|
+
* person adding their own method goes through /user-email and its siblings,
|
|
744
|
+
* where the model's own ownership scoping applies end to end; letting them
|
|
745
|
+
* in here instead would put an ordinary member's self-service on a code
|
|
746
|
+
* path that runs as root.
|
|
747
|
+
*/
|
|
748
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
749
|
+
expect(addSpy).not.toHaveBeenCalled();
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe("adding a method", () => {
|
|
754
|
+
test("passes the channel, the value and the SERVER's actor", async () => {
|
|
755
|
+
const result: RouteCallResult = await callRoute({
|
|
756
|
+
method: "POST",
|
|
757
|
+
uri: ADD_ROUTE,
|
|
758
|
+
params: { userId: targetUserId.toString() },
|
|
759
|
+
body: {
|
|
760
|
+
methodType: "SMS",
|
|
761
|
+
value: " +15551234821 ",
|
|
762
|
+
/*
|
|
763
|
+
* A body that tries to sign the change with somebody else's name. It
|
|
764
|
+
* must not be read: this value ends up in an audit entry and in a mail
|
|
765
|
+
* to the account holder, and a caller-supplied actor would let an
|
|
766
|
+
* attacker attribute their own write to a colleague.
|
|
767
|
+
*/
|
|
768
|
+
actorUserId: ObjectID.generate().toString(),
|
|
769
|
+
userId: ObjectID.generate().toString(),
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
expect(result.nextCallCount).toBe(0);
|
|
774
|
+
|
|
775
|
+
const call: any = addSpy.mock.calls[0]![0];
|
|
776
|
+
|
|
777
|
+
expect(call.projectId.toString()).toBe(projectId.toString());
|
|
778
|
+
expect(call.targetUserId.toString()).toBe(targetUserId.toString());
|
|
779
|
+
expect(call.actorUserId.toString()).toBe(callerUserId.toString());
|
|
780
|
+
expect(call.methodType).toBe("SMS");
|
|
781
|
+
|
|
782
|
+
// Trimmed at the edge, so the service never sees the caller's whitespace.
|
|
783
|
+
expect(call.value).toBe("+15551234821");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("refuses a body with no channel or no value", async () => {
|
|
787
|
+
for (const body of [
|
|
788
|
+
{},
|
|
789
|
+
{ methodType: "SMS" },
|
|
790
|
+
{ value: "+15551234821" },
|
|
791
|
+
{ methodType: "SMS", value: " " },
|
|
792
|
+
{ methodType: 42, value: "+15551234821" },
|
|
793
|
+
] as Array<JSONObject>) {
|
|
794
|
+
addSpy.mockClear();
|
|
795
|
+
|
|
796
|
+
const result: RouteCallResult = await callRoute({
|
|
797
|
+
method: "POST",
|
|
798
|
+
uri: ADD_ROUTE,
|
|
799
|
+
params: { userId: targetUserId.toString() },
|
|
800
|
+
body: body,
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
804
|
+
expect(addSpy).not.toHaveBeenCalled();
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("checks the permission before it reads the body", async () => {
|
|
809
|
+
propsSpy.mockResolvedValue(
|
|
810
|
+
buildProps({
|
|
811
|
+
projectId: projectId,
|
|
812
|
+
userId: callerUserId,
|
|
813
|
+
permissions: [Permission.ProjectMember],
|
|
814
|
+
}) as never,
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
const result: RouteCallResult = await callRoute({
|
|
818
|
+
method: "POST",
|
|
819
|
+
uri: ADD_ROUTE,
|
|
820
|
+
params: { userId: targetUserId.toString() },
|
|
821
|
+
body: {},
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
/*
|
|
825
|
+
* The body here is invalid AND the caller is unauthorised. A route that
|
|
826
|
+
* validated first would answer "methodType is required", which tells a
|
|
827
|
+
* caller it has no business talking to this endpoint what the endpoint
|
|
828
|
+
* expects.
|
|
829
|
+
*/
|
|
830
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
831
|
+
expect(result.thrownToNext).not.toBeInstanceOf(BadDataException);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test("returns the created method, masked", async () => {
|
|
835
|
+
await callRoute({
|
|
836
|
+
method: "POST",
|
|
837
|
+
uri: ADD_ROUTE,
|
|
838
|
+
params: { userId: targetUserId.toString() },
|
|
839
|
+
body: { methodType: "SMS", value: "+15551234821" },
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const payload: Record<string, unknown> = jsonPayload();
|
|
843
|
+
|
|
844
|
+
expect(payload["maskedIdentifier"]).toBe("+1 ••• ••• 4821");
|
|
845
|
+
|
|
846
|
+
/*
|
|
847
|
+
* And UNVERIFIED. The whole safety property of this feature is that an
|
|
848
|
+
* administrator can type a number in but cannot make it live — the code
|
|
849
|
+
* goes to the device and the verify endpoints refuse anybody but the row's
|
|
850
|
+
* owner — so a response claiming otherwise would be the first sign that
|
|
851
|
+
* property had been lost.
|
|
852
|
+
*/
|
|
853
|
+
expect(payload["isVerified"]).toBe(false);
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
describe("removing a method", () => {
|
|
858
|
+
test("names the channel and the row, and the server's actor", async () => {
|
|
859
|
+
await callRoute({
|
|
860
|
+
method: "DELETE",
|
|
861
|
+
uri: DELETE_ROUTE,
|
|
862
|
+
params: {
|
|
863
|
+
userId: targetUserId.toString(),
|
|
864
|
+
methodType: "SMS",
|
|
865
|
+
methodId: methodId.toString(),
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const call: any = deleteSpy.mock.calls[0]![0];
|
|
870
|
+
|
|
871
|
+
expect(call.projectId.toString()).toBe(projectId.toString());
|
|
872
|
+
expect(call.targetUserId.toString()).toBe(targetUserId.toString());
|
|
873
|
+
expect(call.actorUserId.toString()).toBe(callerUserId.toString());
|
|
874
|
+
expect(call.methodType).toBe("SMS");
|
|
875
|
+
expect(call.methodId.toString()).toBe(methodId.toString());
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("rejects a method id that is not a uuid", async () => {
|
|
879
|
+
const result: RouteCallResult = await callRoute({
|
|
880
|
+
method: "DELETE",
|
|
881
|
+
uri: DELETE_ROUTE,
|
|
882
|
+
params: {
|
|
883
|
+
userId: targetUserId.toString(),
|
|
884
|
+
methodType: "SMS",
|
|
885
|
+
methodId: "not-a-uuid",
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
890
|
+
expect(deleteSpy).not.toHaveBeenCalled();
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
test("the preview crosses the wire as plain numbers", async () => {
|
|
894
|
+
await callRoute({
|
|
895
|
+
method: "GET",
|
|
896
|
+
uri: IMPACT_ROUTE,
|
|
897
|
+
params: {
|
|
898
|
+
userId: targetUserId.toString(),
|
|
899
|
+
methodType: "SMS",
|
|
900
|
+
methodId: methodId.toString(),
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
const payload: Record<string, unknown> = jsonPayload();
|
|
905
|
+
|
|
906
|
+
expect(payload["rulesDeletedCount"]).toBe(2);
|
|
907
|
+
expect(payload["coverageLostCount"]).toBe(1);
|
|
908
|
+
expect(payload["verifiedMethodCountAfterDeletion"]).toBe(0);
|
|
909
|
+
|
|
910
|
+
/*
|
|
911
|
+
* Carried through rather than swallowed: a confirmation that silently
|
|
912
|
+
* undercounts is worse than one that says it is unsure.
|
|
913
|
+
*/
|
|
914
|
+
expect(payload["isTruncated"]).toBe(false);
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
describe("the project scope", () => {
|
|
919
|
+
test("every route checks the target against the caller's own project", async () => {
|
|
920
|
+
await callRoute({
|
|
921
|
+
method: "GET",
|
|
922
|
+
uri: LIST_ROUTE,
|
|
923
|
+
params: { userId: targetUserId.toString() },
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
const call: any = teamMemberFindBy.mock.calls[0]![0];
|
|
927
|
+
|
|
928
|
+
/*
|
|
929
|
+
* The project comes from the props the server resolved for this caller, not
|
|
930
|
+
* from anything in the path — otherwise a member of project A reads project
|
|
931
|
+
* B's data simply by sending their own `tenantid` header alongside a
|
|
932
|
+
* borrowed user id.
|
|
933
|
+
*/
|
|
934
|
+
expect(call.query.projectId.toString()).toBe(projectId.toString());
|
|
935
|
+
expect(call.query.projectId.toString()).not.toBe(otherProjectId.toString());
|
|
936
|
+
expect(call.query.userId.toString()).toBe(targetUserId.toString());
|
|
937
|
+
expect(call.props.isRoot).toBe(true);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("a caller with no project is refused everywhere", async () => {
|
|
941
|
+
propsSpy.mockResolvedValue({
|
|
942
|
+
userId: callerUserId,
|
|
943
|
+
userTenantAccessPermission: {},
|
|
944
|
+
} as never);
|
|
945
|
+
|
|
946
|
+
for (const uri of [LIST_ROUTE, ADD_ROUTE]) {
|
|
947
|
+
const result: RouteCallResult = await callRoute({
|
|
948
|
+
method: uri === LIST_ROUTE ? "GET" : "POST",
|
|
949
|
+
uri: uri,
|
|
950
|
+
params: { userId: targetUserId.toString() },
|
|
951
|
+
body: { methodType: "Email", value: "someone@example.com" },
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
expect(result.nextCallCount).toBe(1);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
for (const spy of everyWriteSpy()) {
|
|
958
|
+
expect(spy).not.toHaveBeenCalled();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
expect(listSpy).not.toHaveBeenCalled();
|
|
962
|
+
});
|
|
963
|
+
});
|