@oneuptime/common 13.0.8 → 14.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AuditLog.ts +23 -15
- package/Models/DatabaseModels/GlobalConfig.ts +24 -0
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/Monitor.ts +3 -2
- package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
- package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
- package/Scripts/generate-service-worker.js +65 -4
- package/Server/API/BaseAPI.ts +26 -5
- package/Server/API/GlobalConfigAPI.ts +228 -462
- package/Server/API/ProjectOIDC.ts +25 -16
- package/Server/API/ProjectSSO.ts +24 -16
- package/Server/API/StatusPageAPI.ts +100 -42
- package/Server/API/TeamMemberAPI.ts +10 -1
- package/Server/Enterprise/EnterpriseEdition.ts +649 -0
- package/Server/Enterprise/EnterpriseFeature.ts +48 -0
- package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
- package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
- package/Server/EnvironmentConfig.ts +147 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/UserAuthorization.ts +38 -16
- package/Server/Services/AuditLogService.ts +84 -1079
- package/Server/Services/DatabaseService.ts +10 -0
- package/Server/Services/EnterpriseLicenseService.ts +38 -0
- package/Server/Services/GlobalConfigService.ts +80 -1
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/MonitorProbeService.ts +116 -20
- package/Server/Services/MonitorService.ts +59 -6
- package/Server/Services/MonitorStatusTimelineService.ts +284 -0
- package/Server/Services/MonitorTemplateService.ts +26 -0
- package/Server/Services/ProjectService.ts +284 -0
- package/Server/Services/RumSessionPinService.ts +25 -6
- package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
- package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
- package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
- package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
- package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
- package/Server/Services/StatusPagePrivateUserService.ts +7 -3
- package/Server/Services/StatusPageService.ts +78 -1
- package/Server/Services/TeamMemberService.ts +11 -0
- package/Server/Services/TeamService.ts +10 -0
- package/Server/Services/UserService.ts +10 -17
- package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
- package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
- package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
- package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
- package/Server/Types/Markdown.ts +41 -0
- package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
- package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
- package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
- package/Server/Utils/EditionEnforcement.ts +233 -0
- package/Server/Utils/FrontendEnvironment.ts +41 -2
- package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
- package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
- package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
- package/Server/Utils/Monitor/RootCauseList.ts +139 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
- package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
- package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
- package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
- package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
- package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
- package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
- package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
- package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
- package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
- package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
- package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
- package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
- package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
- package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
- package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
- package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
- package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
- package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
- package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
- package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
- package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
- package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
- package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
- package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
- package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
- package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
- package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
- package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
- package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
- package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
- package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
- package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
- package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
- package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
- package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
- package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
- package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
- package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
- package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
- package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
- package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
- package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
- package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
- package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
- package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
- package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
- package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
- package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
- package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
- package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
- package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
- package/Tests/Server/Services/AuditLogService.test.ts +460 -990
- package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
- package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
- package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
- package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
- package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
- package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
- package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
- package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
- package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
- package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
- package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
- package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
- package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
- package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
- package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
- package/Tests/Server/Types/Markdown.test.ts +183 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
- package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
- package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
- package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
- package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
- package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
- package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
- package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
- package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
- package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
- package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
- package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
- package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
- package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
- package/Tests/Types/API/Route.test.ts +56 -0
- package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
- package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
- package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
- package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
- package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
- package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
- package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
- package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
- package/Tests/UI/Components/Icon.test.tsx +26 -0
- package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
- package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
- package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
- package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
- package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
- package/Tests/Utils/HostAddressUtil.test.ts +138 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
- package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
- package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
- package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
- package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
- package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
- package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
- package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
- package/Types/API/Hostname.ts +63 -16
- package/Types/API/Route.ts +19 -2
- package/Types/API/URL.ts +26 -2
- package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
- package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
- package/Types/Database/CanonicalModelInstance.ts +102 -0
- package/Types/Database/TableColumn.ts +20 -6
- package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
- package/Types/IP/IPv6.ts +6 -1
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
- package/Types/Monitor/MonitorStep.ts +6 -1
- package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
- package/Types/Permission.ts +90 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
- package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
- package/UI/Components/Feed/FeedOptions.ts +427 -0
- package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
- package/UI/Components/Feed/useFeedItems.ts +28 -9
- package/UI/Components/Feed/useFeedOptions.ts +225 -0
- package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
- package/UI/Components/ModelList/ModelList.tsx +11 -1
- package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
- package/UI/esbuild-enterprise.js +263 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
- package/Utils/HostAddressUtil.ts +94 -0
- package/Utils/LabelRuleImportExport.ts +27 -9
- package/Utils/Monitor/MetricValueFormatter.ts +2 -2
- package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
- package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
- package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
- package/Utils/Slo/SloDuration.ts +22 -6
- package/Utils/Slo/SloHealth.ts +8 -7
- package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
- package/Utils/Slo/SloOverviewText.ts +33 -0
- package/Utils/Slo/SloProjection.ts +4 -3
- package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
- package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
- package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
- package/build/dist/Server/API/BaseAPI.js +26 -5
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/ProjectOIDC.js +24 -16
- package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
- package/build/dist/Server/API/ProjectSSO.js +24 -16
- package/build/dist/Server/API/ProjectSSO.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +93 -43
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +8 -1
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
- package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
- package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
- package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
- package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
- package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
- package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
- package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +84 -2
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.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/UserAuthorization.js +30 -11
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AuditLogService.js +71 -740
- package/build/dist/Server/Services/AuditLogService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +7 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/GlobalConfigService.js +63 -0
- package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorProbeService.js +90 -17
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +48 -6
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +214 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RumSessionPinService.js +24 -6
- package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
- package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +71 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +10 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +9 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +10 -16
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
- package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
- package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
- package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +31 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
- package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
- package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
- package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
- package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
- package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
- package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
- package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
- package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
- package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +59 -11
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/API/Route.js +18 -2
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/API/URL.js +23 -2
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
- package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
- package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
- package/build/dist/Types/Database/TableColumn.js +19 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
- package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
- package/build/dist/Types/IP/IPv6.js +6 -1
- package/build/dist/Types/IP/IPv6.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +5 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
- package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
- package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
- package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
- package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
- package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
- package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
- package/build/dist/Utils/HostAddressUtil.js +80 -0
- package/build/dist/Utils/HostAddressUtil.js.map +1 -0
- package/build/dist/Utils/LabelRuleImportExport.js +22 -6
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
- package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
- package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
- package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
- package/build/dist/Utils/Slo/SloDuration.js +13 -5
- package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
- package/build/dist/Utils/Slo/SloHealth.js +8 -7
- package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
- package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
- package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
- package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
- package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
- package/jest.config.json +3 -1
- package/package.json +1 -1
- package/test-setup.sh +103 -34
- package/tsconfig.json +13 -0
- package/Server/API/EnterpriseLicenseAPI.ts +0 -925
- package/Server/API/TeamComplianceAPI.ts +0 -158
- package/Server/Middleware/SCIMAuthorization.ts +0 -141
- package/Server/Services/TeamComplianceService.ts +0 -733
- package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
- package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
- package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
- package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
- package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
- package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
- package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
- package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
- package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
- package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
- package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
- package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
- package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
- package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
- package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
- package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
- package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
- package/build/dist/Server/Services/TeamComplianceService.js +0 -537
- package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
- package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
- package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
- package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
|
@@ -1,1851 +0,0 @@
|
|
|
1
|
-
import EnterpriseLicenseAPI from "../../../Server/API/EnterpriseLicenseAPI";
|
|
2
|
-
import EnterpriseLicenseService from "../../../Server/Services/EnterpriseLicenseService";
|
|
3
|
-
import EnterpriseLicenseInstanceService from "../../../Server/Services/EnterpriseLicenseInstanceService";
|
|
4
|
-
import JSONWebToken from "../../../Server/Utils/JsonWebToken";
|
|
5
|
-
import Response from "../../../Server/Utils/Response";
|
|
6
|
-
import EnterpriseLicense from "../../../Models/DatabaseModels/EnterpriseLicense";
|
|
7
|
-
import EnterpriseLicenseInstance from "../../../Models/DatabaseModels/EnterpriseLicenseInstance";
|
|
8
|
-
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
9
|
-
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
-
import OneUptimeDate from "../../../Types/Date";
|
|
11
|
-
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
12
|
-
import { JSONObject } from "../../../Types/JSON";
|
|
13
|
-
import EnterpriseLicenseSyncUtil, {
|
|
14
|
-
EnterpriseLicenseSyncResult,
|
|
15
|
-
} from "../../../Utils/EnterpriseLicense/EnterpriseLicenseSync";
|
|
16
|
-
import EnterpriseLicenseUsageUtil from "../../../Utils/EnterpriseLicense/EnterpriseLicenseUsage";
|
|
17
|
-
import EnterpriseLicenseUserCountSource from "../../../Types/EnterpriseLicense/EnterpriseLicenseUserCountSource";
|
|
18
|
-
import EnterpriseLicenseInstanceSummary from "../../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
|
|
19
|
-
import MasterAdminAuthorization from "../../../Server/Middleware/MasterAdminAuthorization";
|
|
20
|
-
import {
|
|
21
|
-
NextFunction,
|
|
22
|
-
OneUptimeRequest,
|
|
23
|
-
OneUptimeResponse,
|
|
24
|
-
} from "../../../Server/Utils/Express";
|
|
25
|
-
import { mockRouter } from "./Helpers";
|
|
26
|
-
import { beforeEach, afterEach, describe, expect, it } from "@jest/globals";
|
|
27
|
-
|
|
28
|
-
/*
|
|
29
|
-
* The license-server half of the seat-limit sync.
|
|
30
|
-
*
|
|
31
|
-
* /report-user-count is the daily call a self-hosted installation makes, and
|
|
32
|
-
* its response is the ONLY thing that installation can learn from without a
|
|
33
|
-
* human re-typing the license key. It used to answer with the usage numbers
|
|
34
|
-
* and the seat limit but nothing else, and the job on the other end dropped
|
|
35
|
-
* the seat limit on the floor — so raising a customer's limit on oneuptime.com
|
|
36
|
-
* never reached the installation enforcing it.
|
|
37
|
-
*
|
|
38
|
-
* These tests pin the response as a complete statement of the license terms,
|
|
39
|
-
* and pin the one thing that must NOT be unconditional: an expired license
|
|
40
|
-
* still gets a straight answer (it has to keep reporting, the expiry emails
|
|
41
|
-
* are built from those reports) but it does not get a fresh token.
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
/*
|
|
45
|
-
* PasswordHash carries a pre-existing TS diagnostic that fails any suite whose
|
|
46
|
-
* require graph reaches it, and DatabaseService — the base of every concrete
|
|
47
|
-
* service below — imports it. Replaced with a factory rather than automocked,
|
|
48
|
-
* because an automock still type-checks the real file.
|
|
49
|
-
*/
|
|
50
|
-
jest.mock("../../../Server/Utils/PasswordHash", () => {
|
|
51
|
-
return {
|
|
52
|
-
__esModule: true,
|
|
53
|
-
default: {
|
|
54
|
-
hash: jest.fn(),
|
|
55
|
-
verify: jest.fn(),
|
|
56
|
-
generateSalt: jest.fn(),
|
|
57
|
-
needsUpgrade: jest.fn(),
|
|
58
|
-
applyPepper: jest.fn(),
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
/*
|
|
64
|
-
* Same story as PasswordHash: a pre-existing local-only TS diagnostic in a
|
|
65
|
-
* module the service graph drags in. Nothing here verifies a code.
|
|
66
|
-
*/
|
|
67
|
-
jest.mock("../../../Server/Utils/VerificationCode", () => {
|
|
68
|
-
return {
|
|
69
|
-
__esModule: true,
|
|
70
|
-
default: {
|
|
71
|
-
generate: jest.fn(),
|
|
72
|
-
hashCode: jest.fn(),
|
|
73
|
-
isHashEqual: jest.fn(),
|
|
74
|
-
generateUnusableHash: jest.fn(),
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
jest.mock("../../../Server/Utils/Express", () => {
|
|
80
|
-
return {
|
|
81
|
-
getRouter: () => {
|
|
82
|
-
return mockRouter;
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
jest.mock("../../../Server/Utils/Response", () => {
|
|
88
|
-
return {
|
|
89
|
-
sendEntityArrayResponse: jest.fn().mockImplementation((...args: []) => {
|
|
90
|
-
return args;
|
|
91
|
-
}),
|
|
92
|
-
sendJsonObjectResponse: jest.fn().mockImplementation((...args: []) => {
|
|
93
|
-
return args;
|
|
94
|
-
}),
|
|
95
|
-
sendEmptySuccessResponse: jest.fn(),
|
|
96
|
-
sendEntityResponse: jest.fn().mockImplementation((...args: []) => {
|
|
97
|
-
return args;
|
|
98
|
-
}),
|
|
99
|
-
sendErrorResponse: jest.fn().mockImplementation((...args: []) => {
|
|
100
|
-
return args;
|
|
101
|
-
}),
|
|
102
|
-
};
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
/*
|
|
106
|
-
* Signed with the server's own secret in production; here it only has to be
|
|
107
|
-
* deterministic and countable, so the expiry gating can be asserted on whether
|
|
108
|
-
* it was called at all.
|
|
109
|
-
*/
|
|
110
|
-
jest.mock("../../../Server/Utils/JsonWebToken", () => {
|
|
111
|
-
return {
|
|
112
|
-
__esModule: true,
|
|
113
|
-
default: {
|
|
114
|
-
signJsonPayload: jest.fn().mockReturnValue("signed.jwt.token"),
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
jest.mock("../../../Server/Services/EnterpriseLicenseService");
|
|
120
|
-
jest.mock("../../../Server/Services/EnterpriseLicenseInstanceService");
|
|
121
|
-
|
|
122
|
-
const REPORT_ROUTE: string = "/enterprise-license/report-user-count";
|
|
123
|
-
const VALIDATE_ROUTE: string = "/enterprise-license/validate";
|
|
124
|
-
const ACTIVE_USAGE_ROUTE: string =
|
|
125
|
-
"/enterprise-license/:enterpriseLicenseId/active-usage";
|
|
126
|
-
|
|
127
|
-
const LICENSE_ID: ObjectID = ObjectID.generate();
|
|
128
|
-
const LICENSE_KEY: string = "acme-license-key";
|
|
129
|
-
|
|
130
|
-
// A hash the sanitizer accepts: 64 hex characters.
|
|
131
|
-
type HashFunction = (seed: string) => string;
|
|
132
|
-
|
|
133
|
-
const hashOf: HashFunction = (seed: string): string => {
|
|
134
|
-
return seed
|
|
135
|
-
.repeat(64)
|
|
136
|
-
.replace(/[^a-f0-9]/g, "a")
|
|
137
|
-
.substring(0, 64);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/*
|
|
141
|
-
* Overrides are loosely typed on purpose: several tests need to express
|
|
142
|
-
* "this column is not set", which exactOptionalPropertyTypes forbids on
|
|
143
|
-
* Partial<EnterpriseLicense>.
|
|
144
|
-
*/
|
|
145
|
-
type MakeLicenseFunction = (
|
|
146
|
-
overrides?: Record<string, unknown>,
|
|
147
|
-
) => EnterpriseLicense;
|
|
148
|
-
|
|
149
|
-
const makeLicense: MakeLicenseFunction = (
|
|
150
|
-
overrides?: Record<string, unknown>,
|
|
151
|
-
): EnterpriseLicense => {
|
|
152
|
-
return {
|
|
153
|
-
id: LICENSE_ID,
|
|
154
|
-
companyName: "Acme Inc",
|
|
155
|
-
licenseKey: LICENSE_KEY,
|
|
156
|
-
expiresAt: OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), 90),
|
|
157
|
-
userLimit: 150,
|
|
158
|
-
currentUserCount: 42,
|
|
159
|
-
userCountUpdatedAt: OneUptimeDate.getCurrentDate(),
|
|
160
|
-
isEvaluationLicense: false,
|
|
161
|
-
...overrides,
|
|
162
|
-
} as unknown as EnterpriseLicense;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
type MakeInstanceFunction = (
|
|
166
|
-
overrides?: Record<string, unknown>,
|
|
167
|
-
) => EnterpriseLicenseInstance;
|
|
168
|
-
|
|
169
|
-
interface LicenseUsageReportResultForTest {
|
|
170
|
-
reportedAt: Date;
|
|
171
|
-
instances: Array<EnterpriseLicenseInstance>;
|
|
172
|
-
currentUserCount: number;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const makeInstance: MakeInstanceFunction = (
|
|
176
|
-
overrides?: Record<string, unknown>,
|
|
177
|
-
): EnterpriseLicenseInstance => {
|
|
178
|
-
return {
|
|
179
|
-
id: ObjectID.generate(),
|
|
180
|
-
instanceId: "instance-1",
|
|
181
|
-
host: "oneuptime.acme.internal",
|
|
182
|
-
userCount: 2,
|
|
183
|
-
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
184
|
-
lastReportedAt: OneUptimeDate.getCurrentDate(),
|
|
185
|
-
oneuptimeVersion: "12.0.19",
|
|
186
|
-
...overrides,
|
|
187
|
-
} as unknown as EnterpriseLicenseInstance;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
type GetResponseBodyFunction = () => JSONObject;
|
|
191
|
-
|
|
192
|
-
/*
|
|
193
|
-
* The third argument of the single sendJsonObjectResponse call: the literal
|
|
194
|
-
* body that goes on the wire.
|
|
195
|
-
*/
|
|
196
|
-
const getResponseBody: GetResponseBodyFunction = (): JSONObject => {
|
|
197
|
-
const calls: Array<Array<unknown>> = (
|
|
198
|
-
Response.sendJsonObjectResponse as unknown as jest.Mock
|
|
199
|
-
).mock.calls as Array<Array<unknown>>;
|
|
200
|
-
|
|
201
|
-
expect(calls).toHaveLength(1);
|
|
202
|
-
|
|
203
|
-
return calls[0]![2] as JSONObject;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
describe("EnterpriseLicenseAPI POST /enterprise-license/report-user-count", () => {
|
|
207
|
-
let mockRequest: OneUptimeRequest;
|
|
208
|
-
let mockResponse: OneUptimeResponse;
|
|
209
|
-
let nextFunction: NextFunction;
|
|
210
|
-
|
|
211
|
-
type CallRouteFunction = (route?: string) => Promise<void>;
|
|
212
|
-
|
|
213
|
-
const callRoute: CallRouteFunction = async (
|
|
214
|
-
route: string = REPORT_ROUTE,
|
|
215
|
-
): Promise<void> => {
|
|
216
|
-
await mockRouter
|
|
217
|
-
.match("post", route)
|
|
218
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
beforeEach(() => {
|
|
222
|
-
jest.clearAllMocks();
|
|
223
|
-
|
|
224
|
-
new EnterpriseLicenseAPI();
|
|
225
|
-
|
|
226
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
227
|
-
.fn()
|
|
228
|
-
.mockResolvedValue(makeLicense());
|
|
229
|
-
EnterpriseLicenseService.updateOneById = jest
|
|
230
|
-
.fn()
|
|
231
|
-
.mockResolvedValue(undefined);
|
|
232
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
233
|
-
.fn()
|
|
234
|
-
.mockImplementation(
|
|
235
|
-
async (data: { fn: () => Promise<unknown> }): Promise<unknown> => {
|
|
236
|
-
return await data.fn();
|
|
237
|
-
},
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([]);
|
|
241
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
242
|
-
.fn()
|
|
243
|
-
.mockResolvedValue(null);
|
|
244
|
-
EnterpriseLicenseInstanceService.updateOneById = jest
|
|
245
|
-
.fn()
|
|
246
|
-
.mockResolvedValue(undefined);
|
|
247
|
-
EnterpriseLicenseInstanceService.create = jest
|
|
248
|
-
.fn()
|
|
249
|
-
.mockResolvedValue(undefined);
|
|
250
|
-
EnterpriseLicenseInstanceService.countBy = jest
|
|
251
|
-
.fn()
|
|
252
|
-
.mockResolvedValue(new PositiveNumber(0));
|
|
253
|
-
|
|
254
|
-
mockRequest = {
|
|
255
|
-
body: {
|
|
256
|
-
licenseKey: LICENSE_KEY,
|
|
257
|
-
userCount: 2,
|
|
258
|
-
instanceId: "instance-1",
|
|
259
|
-
host: "oneuptime.acme.internal",
|
|
260
|
-
version: "12.0.19",
|
|
261
|
-
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
262
|
-
masterAdminEmails: ["admin@acme.com"],
|
|
263
|
-
},
|
|
264
|
-
} as unknown as OneUptimeRequest;
|
|
265
|
-
|
|
266
|
-
mockResponse = {
|
|
267
|
-
send: jest.fn(),
|
|
268
|
-
json: jest.fn(),
|
|
269
|
-
status: jest.fn().mockReturnThis(),
|
|
270
|
-
} as unknown as OneUptimeResponse;
|
|
271
|
-
|
|
272
|
-
nextFunction = jest.fn();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
afterEach(() => {
|
|
276
|
-
jest.restoreAllMocks();
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe("the license terms it reports back", () => {
|
|
280
|
-
it("returns the seat limit, which is the field the installation was missing", async () => {
|
|
281
|
-
await callRoute();
|
|
282
|
-
|
|
283
|
-
expect(getResponseBody()["userLimit"]).toBe(150);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("returns null for a license with no seat limit, so the limit can be cleared", async () => {
|
|
287
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
288
|
-
.fn()
|
|
289
|
-
.mockResolvedValue(makeLicense({ userLimit: undefined }));
|
|
290
|
-
|
|
291
|
-
await callRoute();
|
|
292
|
-
|
|
293
|
-
expect(getResponseBody()).toHaveProperty("userLimit", null);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("returns the company name and the expiry, so a renewal reaches the installation", async () => {
|
|
297
|
-
const expiresAt: Date = new Date("2027-01-01T00:00:00.000Z");
|
|
298
|
-
|
|
299
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
300
|
-
.fn()
|
|
301
|
-
.mockResolvedValue(makeLicense({ expiresAt: expiresAt }));
|
|
302
|
-
|
|
303
|
-
await callRoute();
|
|
304
|
-
|
|
305
|
-
const body: JSONObject = getResponseBody();
|
|
306
|
-
|
|
307
|
-
expect(body["companyName"]).toBe("Acme Inc");
|
|
308
|
-
expect(body["expiresAt"]).toBe("2027-01-01T00:00:00.000Z");
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("selects the term columns it reports - a trimmed select is how this broke", async () => {
|
|
312
|
-
await callRoute();
|
|
313
|
-
|
|
314
|
-
expect(EnterpriseLicenseService.findOneBy).toHaveBeenCalledWith(
|
|
315
|
-
expect.objectContaining({
|
|
316
|
-
query: { licenseKey: LICENSE_KEY },
|
|
317
|
-
select: expect.objectContaining({
|
|
318
|
-
companyName: true,
|
|
319
|
-
expiresAt: true,
|
|
320
|
-
licenseKey: true,
|
|
321
|
-
userLimit: true,
|
|
322
|
-
isEvaluationLicense: true,
|
|
323
|
-
}),
|
|
324
|
-
}),
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("mirrors the evaluation flag", async () => {
|
|
329
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
330
|
-
.fn()
|
|
331
|
-
.mockResolvedValue(makeLicense({ isEvaluationLicense: true }));
|
|
332
|
-
|
|
333
|
-
await callRoute();
|
|
334
|
-
|
|
335
|
-
expect(getResponseBody()["isEvaluationLicense"]).toBe(true);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("reports the usage numbers alongside the terms", async () => {
|
|
339
|
-
await callRoute();
|
|
340
|
-
|
|
341
|
-
const body: JSONObject = getResponseBody();
|
|
342
|
-
|
|
343
|
-
expect(body["currentUserCount"]).toBe(2);
|
|
344
|
-
expect(typeof body["userCountUpdatedAt"]).toBe("string");
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
describe("the token", () => {
|
|
349
|
-
it("mints one for a live license", async () => {
|
|
350
|
-
await callRoute();
|
|
351
|
-
|
|
352
|
-
expect(getResponseBody()["token"]).toBe("signed.jwt.token");
|
|
353
|
-
expect(JSONWebToken.signJsonPayload).toHaveBeenCalledTimes(1);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("signs the current terms into it, not a stale copy", async () => {
|
|
357
|
-
await callRoute();
|
|
358
|
-
|
|
359
|
-
expect(JSONWebToken.signJsonPayload).toHaveBeenCalledWith(
|
|
360
|
-
expect.objectContaining({
|
|
361
|
-
companyName: "Acme Inc",
|
|
362
|
-
licenseKey: LICENSE_KEY,
|
|
363
|
-
userLimit: 150,
|
|
364
|
-
}),
|
|
365
|
-
expect.any(Number),
|
|
366
|
-
);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("withholds it once the license has expired, rather than renewing it daily", async () => {
|
|
370
|
-
EnterpriseLicenseService.findOneBy = jest.fn().mockResolvedValue(
|
|
371
|
-
makeLicense({
|
|
372
|
-
expiresAt: OneUptimeDate.addRemoveDays(
|
|
373
|
-
OneUptimeDate.getCurrentDate(),
|
|
374
|
-
-1,
|
|
375
|
-
),
|
|
376
|
-
}),
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
await callRoute();
|
|
380
|
-
|
|
381
|
-
expect(getResponseBody()).toHaveProperty("token", null);
|
|
382
|
-
expect(JSONWebToken.signJsonPayload).not.toHaveBeenCalled();
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("still answers an expired license in full, because it must keep reporting", async () => {
|
|
386
|
-
const expiredAt: Date = OneUptimeDate.addRemoveDays(
|
|
387
|
-
OneUptimeDate.getCurrentDate(),
|
|
388
|
-
-1,
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
392
|
-
.fn()
|
|
393
|
-
.mockResolvedValue(makeLicense({ expiresAt: expiredAt }));
|
|
394
|
-
|
|
395
|
-
await callRoute();
|
|
396
|
-
|
|
397
|
-
const body: JSONObject = getResponseBody();
|
|
398
|
-
|
|
399
|
-
/*
|
|
400
|
-
* The expiry notification emails are built from what expired instances
|
|
401
|
-
* keep reporting, so this route deliberately does not reject them the
|
|
402
|
-
* way /validate does. Telling them the truth about the expiry is what
|
|
403
|
-
* makes the installation show itself as expired.
|
|
404
|
-
*/
|
|
405
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
406
|
-
expect(body["expiresAt"]).toBe(expiredAt.toISOString());
|
|
407
|
-
expect(body["userLimit"]).toBe(150);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it("withholds it for a license with no expiry set at all", async () => {
|
|
411
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
412
|
-
.fn()
|
|
413
|
-
.mockResolvedValue(makeLicense({ expiresAt: undefined }));
|
|
414
|
-
|
|
415
|
-
await callRoute();
|
|
416
|
-
|
|
417
|
-
const body: JSONObject = getResponseBody();
|
|
418
|
-
|
|
419
|
-
expect(body).toHaveProperty("token", null);
|
|
420
|
-
expect(body).toHaveProperty("expiresAt", null);
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
describe("input validation", () => {
|
|
425
|
-
it("rejects a missing license key", async () => {
|
|
426
|
-
mockRequest.body["licenseKey"] = undefined;
|
|
427
|
-
|
|
428
|
-
await callRoute();
|
|
429
|
-
|
|
430
|
-
expect(nextFunction).toHaveBeenCalledWith(
|
|
431
|
-
new BadDataException("License key is required"),
|
|
432
|
-
);
|
|
433
|
-
expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it.each([
|
|
437
|
-
["a missing count", undefined],
|
|
438
|
-
["a negative count", -1],
|
|
439
|
-
["a fractional count", 1.5],
|
|
440
|
-
["a non-numeric count", "many"],
|
|
441
|
-
])("rejects %s", async (_label: string, value: unknown) => {
|
|
442
|
-
mockRequest.body["userCount"] = value;
|
|
443
|
-
|
|
444
|
-
await callRoute();
|
|
445
|
-
|
|
446
|
-
expect(nextFunction).toHaveBeenCalledWith(
|
|
447
|
-
new BadDataException("userCount must be a non-negative integer"),
|
|
448
|
-
);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("accepts a count of zero", async () => {
|
|
452
|
-
mockRequest.body["userCount"] = 0;
|
|
453
|
-
mockRequest.body["userEmailHashes"] = [];
|
|
454
|
-
|
|
455
|
-
await callRoute();
|
|
456
|
-
|
|
457
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
458
|
-
expect(getResponseBody()["currentUserCount"]).toBe(0);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("rejects an unknown license key", async () => {
|
|
462
|
-
EnterpriseLicenseService.findOneBy = jest.fn().mockResolvedValue(null);
|
|
463
|
-
|
|
464
|
-
await callRoute();
|
|
465
|
-
|
|
466
|
-
expect(nextFunction).toHaveBeenCalledWith(
|
|
467
|
-
new BadDataException("License key is invalid"),
|
|
468
|
-
);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it("stays unauthenticated - instances report before anyone signs in", () => {
|
|
472
|
-
expect(mockRouter.match("post", REPORT_ROUTE).middlewares).toHaveLength(
|
|
473
|
-
0,
|
|
474
|
-
);
|
|
475
|
-
});
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
describe("usage accounting", () => {
|
|
479
|
-
it.each([false, true])(
|
|
480
|
-
"keeps instance, license, and dashboard activity aligned when the instance already exists: %s",
|
|
481
|
-
async (alreadyExists: boolean) => {
|
|
482
|
-
const reportedAt: Date = new Date("2026-09-17T08:00:00.000Z");
|
|
483
|
-
const registeredAt: Date = new Date("2026-09-01T08:00:00.000Z");
|
|
484
|
-
const previousReportedAt: Date = new Date("2026-09-16T08:00:00.000Z");
|
|
485
|
-
const instanceId: ObjectID = ObjectID.generate();
|
|
486
|
-
const license: EnterpriseLicense = makeLicense({
|
|
487
|
-
userCountUpdatedAt: previousReportedAt,
|
|
488
|
-
});
|
|
489
|
-
let storedInstance: EnterpriseLicenseInstance | null = alreadyExists
|
|
490
|
-
? makeInstance({
|
|
491
|
-
id: instanceId,
|
|
492
|
-
createdAt: registeredAt,
|
|
493
|
-
lastReportedAt: previousReportedAt,
|
|
494
|
-
})
|
|
495
|
-
: null;
|
|
496
|
-
|
|
497
|
-
jest.spyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(reportedAt);
|
|
498
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
499
|
-
.fn()
|
|
500
|
-
.mockImplementation(
|
|
501
|
-
async (): Promise<EnterpriseLicenseInstance | null> => {
|
|
502
|
-
return storedInstance;
|
|
503
|
-
},
|
|
504
|
-
);
|
|
505
|
-
EnterpriseLicenseInstanceService.create = jest
|
|
506
|
-
.fn()
|
|
507
|
-
.mockImplementation(
|
|
508
|
-
async (args: {
|
|
509
|
-
data: EnterpriseLicenseInstance;
|
|
510
|
-
}): Promise<EnterpriseLicenseInstance> => {
|
|
511
|
-
storedInstance = makeInstance({
|
|
512
|
-
...args.data,
|
|
513
|
-
id: instanceId,
|
|
514
|
-
createdAt: reportedAt,
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
return storedInstance;
|
|
518
|
-
},
|
|
519
|
-
);
|
|
520
|
-
EnterpriseLicenseInstanceService.updateOneById = jest
|
|
521
|
-
.fn()
|
|
522
|
-
.mockImplementation(
|
|
523
|
-
async (args: {
|
|
524
|
-
data: Partial<EnterpriseLicenseInstance>;
|
|
525
|
-
}): Promise<number> => {
|
|
526
|
-
Object.assign(storedInstance!, args.data);
|
|
527
|
-
|
|
528
|
-
return 1;
|
|
529
|
-
},
|
|
530
|
-
);
|
|
531
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
532
|
-
.fn()
|
|
533
|
-
.mockImplementation(
|
|
534
|
-
async (): Promise<Array<EnterpriseLicenseInstance>> => {
|
|
535
|
-
return storedInstance ? [storedInstance] : [];
|
|
536
|
-
},
|
|
537
|
-
);
|
|
538
|
-
EnterpriseLicenseService.updateOneById = jest
|
|
539
|
-
.fn()
|
|
540
|
-
.mockImplementation(
|
|
541
|
-
async (args: {
|
|
542
|
-
data: Partial<EnterpriseLicense>;
|
|
543
|
-
}): Promise<number> => {
|
|
544
|
-
Object.assign(license, args.data);
|
|
545
|
-
|
|
546
|
-
return 1;
|
|
547
|
-
},
|
|
548
|
-
);
|
|
549
|
-
EnterpriseLicenseService.findOneById = jest
|
|
550
|
-
.fn()
|
|
551
|
-
.mockResolvedValue(license);
|
|
552
|
-
|
|
553
|
-
await callRoute();
|
|
554
|
-
|
|
555
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
556
|
-
expect(storedInstance).toEqual(
|
|
557
|
-
expect.objectContaining({
|
|
558
|
-
lastReportedAt: reportedAt,
|
|
559
|
-
createdAt: alreadyExists ? registeredAt : reportedAt,
|
|
560
|
-
}),
|
|
561
|
-
);
|
|
562
|
-
expect(license.userCountUpdatedAt).toEqual(reportedAt);
|
|
563
|
-
expect(getResponseBody()).toEqual(
|
|
564
|
-
expect.objectContaining({
|
|
565
|
-
userCountUpdatedAt: reportedAt.toISOString(),
|
|
566
|
-
instances: [
|
|
567
|
-
expect.objectContaining({
|
|
568
|
-
instanceId: "instance-1",
|
|
569
|
-
lastReportedAt: reportedAt.toISOString(),
|
|
570
|
-
isCountedTowardsUsage: true,
|
|
571
|
-
}),
|
|
572
|
-
],
|
|
573
|
-
}),
|
|
574
|
-
);
|
|
575
|
-
expect(EnterpriseLicenseInstanceService.findBy).toHaveBeenCalledWith(
|
|
576
|
-
expect.objectContaining({
|
|
577
|
-
query: { enterpriseLicenseId: LICENSE_ID },
|
|
578
|
-
select: expect.objectContaining({
|
|
579
|
-
_id: true,
|
|
580
|
-
createdAt: true,
|
|
581
|
-
lastReportedAt: true,
|
|
582
|
-
}),
|
|
583
|
-
}),
|
|
584
|
-
);
|
|
585
|
-
expect(EnterpriseLicenseInstanceService.create).toHaveBeenCalledTimes(
|
|
586
|
-
alreadyExists ? 0 : 1,
|
|
587
|
-
);
|
|
588
|
-
expect(
|
|
589
|
-
EnterpriseLicenseInstanceService.updateOneById,
|
|
590
|
-
).toHaveBeenCalledTimes(alreadyExists ? 1 : 0);
|
|
591
|
-
|
|
592
|
-
jest.mocked(Response.sendJsonObjectResponse).mockClear();
|
|
593
|
-
mockRequest.params = {
|
|
594
|
-
enterpriseLicenseId: LICENSE_ID.toString(),
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
await mockRouter
|
|
598
|
-
.match("get", ACTIVE_USAGE_ROUTE)
|
|
599
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
600
|
-
|
|
601
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
602
|
-
expect(getResponseBody()).toEqual(
|
|
603
|
-
expect.objectContaining({
|
|
604
|
-
currentUserCount: 2,
|
|
605
|
-
activeInstanceIds: [instanceId.toString()],
|
|
606
|
-
lastUsageReportedAt: reportedAt.toISOString(),
|
|
607
|
-
}),
|
|
608
|
-
);
|
|
609
|
-
},
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
it("records this instance's usage against the license", async () => {
|
|
613
|
-
await callRoute();
|
|
614
|
-
|
|
615
|
-
expect(EnterpriseLicenseInstanceService.create).toHaveBeenCalledWith(
|
|
616
|
-
expect.objectContaining({
|
|
617
|
-
data: expect.objectContaining({
|
|
618
|
-
instanceId: "instance-1",
|
|
619
|
-
userCount: 2,
|
|
620
|
-
}),
|
|
621
|
-
}),
|
|
622
|
-
);
|
|
623
|
-
expect(
|
|
624
|
-
EnterpriseLicenseService.runWithUsageAggregationLock,
|
|
625
|
-
).toHaveBeenCalledWith(
|
|
626
|
-
expect.objectContaining({
|
|
627
|
-
licenseId: LICENSE_ID,
|
|
628
|
-
fn: expect.any(Function),
|
|
629
|
-
}),
|
|
630
|
-
);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it("serializes concurrent reports so an older partial snapshot cannot win", async () => {
|
|
634
|
-
const instanceRows: Array<EnterpriseLicenseInstance> = [];
|
|
635
|
-
const events: Array<string> = [];
|
|
636
|
-
let lockTail: Promise<void> = Promise.resolve();
|
|
637
|
-
|
|
638
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
639
|
-
.fn()
|
|
640
|
-
.mockImplementation(
|
|
641
|
-
async (data: {
|
|
642
|
-
fn: () => Promise<LicenseUsageReportResultForTest>;
|
|
643
|
-
}): Promise<LicenseUsageReportResultForTest> => {
|
|
644
|
-
const predecessor: Promise<void> = lockTail;
|
|
645
|
-
let releaseLock: () => void = (): void => {};
|
|
646
|
-
lockTail = new Promise<void>((resolve: () => void) => {
|
|
647
|
-
releaseLock = resolve;
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
await predecessor;
|
|
651
|
-
try {
|
|
652
|
-
return await data.fn();
|
|
653
|
-
} finally {
|
|
654
|
-
releaseLock();
|
|
655
|
-
}
|
|
656
|
-
},
|
|
657
|
-
);
|
|
658
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
659
|
-
.fn()
|
|
660
|
-
.mockImplementation(
|
|
661
|
-
async (args: {
|
|
662
|
-
query: { instanceId: string };
|
|
663
|
-
}): Promise<EnterpriseLicenseInstance | null> => {
|
|
664
|
-
return (
|
|
665
|
-
instanceRows.find(
|
|
666
|
-
(instance: EnterpriseLicenseInstance): boolean => {
|
|
667
|
-
return instance.instanceId === args.query.instanceId;
|
|
668
|
-
},
|
|
669
|
-
) || null
|
|
670
|
-
);
|
|
671
|
-
},
|
|
672
|
-
);
|
|
673
|
-
EnterpriseLicenseInstanceService.create = jest
|
|
674
|
-
.fn()
|
|
675
|
-
.mockImplementation(
|
|
676
|
-
async (args: {
|
|
677
|
-
data: EnterpriseLicenseInstance;
|
|
678
|
-
}): Promise<EnterpriseLicenseInstance> => {
|
|
679
|
-
instanceRows.push(args.data);
|
|
680
|
-
events.push(`upsert:${args.data.instanceId}`);
|
|
681
|
-
await Promise.resolve();
|
|
682
|
-
return args.data;
|
|
683
|
-
},
|
|
684
|
-
);
|
|
685
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
686
|
-
.fn()
|
|
687
|
-
.mockImplementation(
|
|
688
|
-
async (): Promise<Array<EnterpriseLicenseInstance>> => {
|
|
689
|
-
events.push(
|
|
690
|
-
`read:${instanceRows
|
|
691
|
-
.map((instance: EnterpriseLicenseInstance): string => {
|
|
692
|
-
return instance.instanceId || "";
|
|
693
|
-
})
|
|
694
|
-
.join(",")}`,
|
|
695
|
-
);
|
|
696
|
-
await Promise.resolve();
|
|
697
|
-
return [...instanceRows];
|
|
698
|
-
},
|
|
699
|
-
);
|
|
700
|
-
EnterpriseLicenseService.updateOneById = jest
|
|
701
|
-
.fn()
|
|
702
|
-
.mockImplementation(
|
|
703
|
-
async (args: {
|
|
704
|
-
data: { currentUserCount: number };
|
|
705
|
-
}): Promise<number> => {
|
|
706
|
-
events.push(`write:${args.data.currentUserCount}`);
|
|
707
|
-
await Promise.resolve();
|
|
708
|
-
return 1;
|
|
709
|
-
},
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
const makeConcurrentRequest: (
|
|
713
|
-
instanceId: string,
|
|
714
|
-
hash: string,
|
|
715
|
-
) => OneUptimeRequest = (
|
|
716
|
-
instanceId: string,
|
|
717
|
-
hash: string,
|
|
718
|
-
): OneUptimeRequest => {
|
|
719
|
-
return {
|
|
720
|
-
body: {
|
|
721
|
-
licenseKey: LICENSE_KEY,
|
|
722
|
-
userCount: 1,
|
|
723
|
-
instanceId,
|
|
724
|
-
userEmailHashes: [hash],
|
|
725
|
-
},
|
|
726
|
-
} as unknown as OneUptimeRequest;
|
|
727
|
-
};
|
|
728
|
-
const firstNext: NextFunction = jest.fn();
|
|
729
|
-
const secondNext: NextFunction = jest.fn();
|
|
730
|
-
const route: ReturnType<typeof mockRouter.match> = mockRouter.match(
|
|
731
|
-
"post",
|
|
732
|
-
REPORT_ROUTE,
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
await Promise.all([
|
|
736
|
-
route.handlerFunction(
|
|
737
|
-
makeConcurrentRequest("instance-a", hashOf("a")),
|
|
738
|
-
mockResponse,
|
|
739
|
-
firstNext,
|
|
740
|
-
),
|
|
741
|
-
route.handlerFunction(
|
|
742
|
-
makeConcurrentRequest("instance-b", hashOf("b")),
|
|
743
|
-
mockResponse,
|
|
744
|
-
secondNext,
|
|
745
|
-
),
|
|
746
|
-
]);
|
|
747
|
-
|
|
748
|
-
expect(firstNext).not.toHaveBeenCalled();
|
|
749
|
-
expect(secondNext).not.toHaveBeenCalled();
|
|
750
|
-
expect(events).toEqual([
|
|
751
|
-
"upsert:instance-a",
|
|
752
|
-
"read:instance-a",
|
|
753
|
-
"write:1",
|
|
754
|
-
"upsert:instance-b",
|
|
755
|
-
"read:instance-a,instance-b",
|
|
756
|
-
"write:2",
|
|
757
|
-
]);
|
|
758
|
-
expect(
|
|
759
|
-
(
|
|
760
|
-
EnterpriseLicenseService.updateOneById as unknown as jest.Mock
|
|
761
|
-
).mock.calls.map((call: Array<unknown>): number => {
|
|
762
|
-
return (call[0] as { data: { currentUserCount: number } }).data
|
|
763
|
-
.currentUserCount;
|
|
764
|
-
}),
|
|
765
|
-
).toEqual([1, 2]);
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
it("counts a user on two instances once", async () => {
|
|
769
|
-
const shared: string = hashOf("1");
|
|
770
|
-
|
|
771
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
772
|
-
makeInstance({
|
|
773
|
-
instanceId: "instance-1",
|
|
774
|
-
userCount: 2,
|
|
775
|
-
userEmailHashes: [shared, hashOf("2")],
|
|
776
|
-
}),
|
|
777
|
-
makeInstance({
|
|
778
|
-
instanceId: "instance-2",
|
|
779
|
-
userCount: 2,
|
|
780
|
-
userEmailHashes: [shared, hashOf("3")],
|
|
781
|
-
}),
|
|
782
|
-
]);
|
|
783
|
-
|
|
784
|
-
await callRoute();
|
|
785
|
-
|
|
786
|
-
// Union of {1,2} and {1,3} is three seats, not four.
|
|
787
|
-
expect(getResponseBody()["currentUserCount"]).toBe(3);
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
it("writes the deduplicated count back onto the license row", async () => {
|
|
791
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
792
|
-
.fn()
|
|
793
|
-
.mockResolvedValue([makeInstance()]);
|
|
794
|
-
|
|
795
|
-
await callRoute();
|
|
796
|
-
|
|
797
|
-
expect(EnterpriseLicenseService.updateOneById).toHaveBeenCalledWith(
|
|
798
|
-
expect.objectContaining({
|
|
799
|
-
id: LICENSE_ID,
|
|
800
|
-
data: expect.objectContaining({ currentUserCount: 2 }),
|
|
801
|
-
}),
|
|
802
|
-
);
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it("never writes the seat limit back from a report", async () => {
|
|
806
|
-
/*
|
|
807
|
-
* The limit flows one way only: oneuptime.com to the installation. A
|
|
808
|
-
* report that could raise it would make the license meaningless.
|
|
809
|
-
*/
|
|
810
|
-
await callRoute();
|
|
811
|
-
|
|
812
|
-
const updateCall: JSONObject = (
|
|
813
|
-
EnterpriseLicenseService.updateOneById as unknown as jest.Mock
|
|
814
|
-
).mock.calls[0]![0] as JSONObject;
|
|
815
|
-
|
|
816
|
-
expect(Object.keys(updateCall["data"] as JSONObject)).toEqual([
|
|
817
|
-
"currentUserCount",
|
|
818
|
-
"userCountUpdatedAt",
|
|
819
|
-
"userCountSource",
|
|
820
|
-
]);
|
|
821
|
-
expect((updateCall["data"] as JSONObject)["userCountSource"]).toBe(
|
|
822
|
-
EnterpriseLicenseUserCountSource.Instance,
|
|
823
|
-
);
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
it("marks a week-silent instance inactive, excludes its users, and still lists it", async () => {
|
|
827
|
-
const reportedAt: Date = new Date("2026-09-02T12:00:00.000Z");
|
|
828
|
-
|
|
829
|
-
jest.spyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(reportedAt);
|
|
830
|
-
|
|
831
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
832
|
-
makeInstance({
|
|
833
|
-
instanceId: "live",
|
|
834
|
-
userCount: 1,
|
|
835
|
-
userEmailHashes: [hashOf("1")],
|
|
836
|
-
lastReportedAt: reportedAt,
|
|
837
|
-
}),
|
|
838
|
-
makeInstance({
|
|
839
|
-
instanceId: "inactive",
|
|
840
|
-
userCount: 2,
|
|
841
|
-
userEmailHashes: [hashOf("2"), hashOf("3")],
|
|
842
|
-
lastReportedAt: new Date(
|
|
843
|
-
reportedAt.getTime() -
|
|
844
|
-
EnterpriseLicenseUsageUtil.InstanceUsageFreshnessInDays *
|
|
845
|
-
24 *
|
|
846
|
-
60 *
|
|
847
|
-
60 *
|
|
848
|
-
1000,
|
|
849
|
-
),
|
|
850
|
-
}),
|
|
851
|
-
]);
|
|
852
|
-
|
|
853
|
-
await callRoute();
|
|
854
|
-
|
|
855
|
-
const body: JSONObject = getResponseBody();
|
|
856
|
-
|
|
857
|
-
expect(body["currentUserCount"]).toBe(1);
|
|
858
|
-
expect(body["instances"]).toHaveLength(2);
|
|
859
|
-
expect(body["instances"]).toEqual(
|
|
860
|
-
expect.arrayContaining([
|
|
861
|
-
expect.objectContaining({
|
|
862
|
-
instanceId: "live",
|
|
863
|
-
isCountedTowardsUsage: true,
|
|
864
|
-
}),
|
|
865
|
-
expect.objectContaining({
|
|
866
|
-
instanceId: "inactive",
|
|
867
|
-
userCount: 2,
|
|
868
|
-
isCountedTowardsUsage: false,
|
|
869
|
-
}),
|
|
870
|
-
]),
|
|
871
|
-
);
|
|
872
|
-
expect(EnterpriseLicenseService.updateOneById).toHaveBeenCalledWith(
|
|
873
|
-
expect.objectContaining({
|
|
874
|
-
id: LICENSE_ID,
|
|
875
|
-
data: expect.objectContaining({ currentUserCount: 1 }),
|
|
876
|
-
}),
|
|
877
|
-
);
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
it("counts users an instance could not send hashes for, so a huge install is not undercounted", async () => {
|
|
881
|
-
/*
|
|
882
|
-
* Hash lists are capped, so an instance may report more users than
|
|
883
|
-
* hashes. The overflow cannot be deduplicated against anyone else and is
|
|
884
|
-
* counted as-is.
|
|
885
|
-
*/
|
|
886
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
887
|
-
makeInstance({
|
|
888
|
-
instanceId: "big",
|
|
889
|
-
userCount: 10,
|
|
890
|
-
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
891
|
-
}),
|
|
892
|
-
]);
|
|
893
|
-
|
|
894
|
-
await callRoute();
|
|
895
|
-
|
|
896
|
-
expect(getResponseBody()["currentUserCount"]).toBe(10);
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
it("retains a legacy heartbeat without letting it stomp the active modern count", async () => {
|
|
900
|
-
mockRequest.body["instanceId"] = undefined;
|
|
901
|
-
mockRequest.body["userCount"] = 99;
|
|
902
|
-
|
|
903
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
904
|
-
.fn()
|
|
905
|
-
.mockResolvedValue([makeInstance()]);
|
|
906
|
-
|
|
907
|
-
await callRoute();
|
|
908
|
-
|
|
909
|
-
expect(getResponseBody()["currentUserCount"]).toBe(2);
|
|
910
|
-
expect(EnterpriseLicenseService.updateOneById).toHaveBeenCalledWith(
|
|
911
|
-
expect.objectContaining({
|
|
912
|
-
id: LICENSE_ID,
|
|
913
|
-
data: {
|
|
914
|
-
legacyUserCount: 99,
|
|
915
|
-
legacyUserCountUpdatedAt: expect.any(Date),
|
|
916
|
-
},
|
|
917
|
-
}),
|
|
918
|
-
);
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
it("accepts a live legacy report after every modern instance becomes inactive", async () => {
|
|
922
|
-
const reportedAt: Date = new Date("2026-09-02T12:00:00.000Z");
|
|
923
|
-
jest.spyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(reportedAt);
|
|
924
|
-
mockRequest.body["instanceId"] = undefined;
|
|
925
|
-
mockRequest.body["userCount"] = 19;
|
|
926
|
-
|
|
927
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
928
|
-
makeInstance({
|
|
929
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(reportedAt, -7),
|
|
930
|
-
userCount: 50,
|
|
931
|
-
}),
|
|
932
|
-
]);
|
|
933
|
-
|
|
934
|
-
await callRoute();
|
|
935
|
-
|
|
936
|
-
expect(getResponseBody()["currentUserCount"]).toBe(19);
|
|
937
|
-
expect(EnterpriseLicenseService.updateOneById).toHaveBeenCalledWith(
|
|
938
|
-
expect.objectContaining({
|
|
939
|
-
data: expect.objectContaining({
|
|
940
|
-
currentUserCount: 19,
|
|
941
|
-
legacyUserCount: 19,
|
|
942
|
-
legacyUserCountUpdatedAt: reportedAt,
|
|
943
|
-
userCountSource: EnterpriseLicenseUserCountSource.Legacy,
|
|
944
|
-
}),
|
|
945
|
-
}),
|
|
946
|
-
);
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
it("accepts a legacy report while the only tracked instance has registered but not reported usage", async () => {
|
|
950
|
-
mockRequest.body["instanceId"] = undefined;
|
|
951
|
-
mockRequest.body["userCount"] = 19;
|
|
952
|
-
|
|
953
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
954
|
-
makeInstance({
|
|
955
|
-
createdAt: OneUptimeDate.addRemoveDays(
|
|
956
|
-
OneUptimeDate.getCurrentDate(),
|
|
957
|
-
-1,
|
|
958
|
-
),
|
|
959
|
-
lastReportedAt: undefined,
|
|
960
|
-
userCount: undefined,
|
|
961
|
-
userEmailHashes: undefined,
|
|
962
|
-
}),
|
|
963
|
-
]);
|
|
964
|
-
|
|
965
|
-
await callRoute();
|
|
966
|
-
|
|
967
|
-
expect(getResponseBody()["currentUserCount"]).toBe(19);
|
|
968
|
-
expect(EnterpriseLicenseService.updateOneById).toHaveBeenCalledWith(
|
|
969
|
-
expect.objectContaining({
|
|
970
|
-
id: LICENSE_ID,
|
|
971
|
-
data: expect.objectContaining({ currentUserCount: 19 }),
|
|
972
|
-
}),
|
|
973
|
-
);
|
|
974
|
-
});
|
|
975
|
-
});
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
describe("EnterpriseLicenseAPI GET active usage snapshot", () => {
|
|
979
|
-
let mockRequest: OneUptimeRequest;
|
|
980
|
-
let mockResponse: OneUptimeResponse;
|
|
981
|
-
let nextFunction: NextFunction;
|
|
982
|
-
const calculatedAt: Date = new Date("2026-09-02T12:00:00.000Z");
|
|
983
|
-
|
|
984
|
-
beforeEach(() => {
|
|
985
|
-
jest.clearAllMocks();
|
|
986
|
-
|
|
987
|
-
new EnterpriseLicenseAPI();
|
|
988
|
-
jest.spyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(calculatedAt);
|
|
989
|
-
|
|
990
|
-
EnterpriseLicenseService.findOneById = jest
|
|
991
|
-
.fn()
|
|
992
|
-
.mockResolvedValue(makeLicense({ userCountUpdatedAt: calculatedAt }));
|
|
993
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
994
|
-
.fn()
|
|
995
|
-
.mockImplementation(
|
|
996
|
-
async (data: { fn: () => Promise<unknown> }): Promise<unknown> => {
|
|
997
|
-
return await data.fn();
|
|
998
|
-
},
|
|
999
|
-
);
|
|
1000
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([]);
|
|
1001
|
-
|
|
1002
|
-
mockRequest = {
|
|
1003
|
-
params: {
|
|
1004
|
-
enterpriseLicenseId: LICENSE_ID.toString(),
|
|
1005
|
-
},
|
|
1006
|
-
} as unknown as OneUptimeRequest;
|
|
1007
|
-
mockResponse = {
|
|
1008
|
-
send: jest.fn(),
|
|
1009
|
-
json: jest.fn(),
|
|
1010
|
-
status: jest.fn().mockReturnThis(),
|
|
1011
|
-
} as unknown as OneUptimeResponse;
|
|
1012
|
-
nextFunction = jest.fn();
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
afterEach(() => {
|
|
1016
|
-
jest.restoreAllMocks();
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
const callRoute: () => Promise<void> = async (): Promise<void> => {
|
|
1020
|
-
await mockRouter
|
|
1021
|
-
.match("get", ACTIVE_USAGE_ROUTE)
|
|
1022
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
it("is restricted to authenticated master admins", () => {
|
|
1026
|
-
expect(mockRouter.match("get", ACTIVE_USAGE_ROUTE).middleware).toBe(
|
|
1027
|
-
MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
|
|
1028
|
-
);
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
it("returns one cutoff-aligned count and active-instance list without hashes", async () => {
|
|
1032
|
-
const active: EnterpriseLicenseInstance = makeInstance({
|
|
1033
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1034
|
-
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
1035
|
-
masterAdminEmails: [" Admin@Acme.com "],
|
|
1036
|
-
});
|
|
1037
|
-
const newlyRegistered: EnterpriseLicenseInstance = makeInstance({
|
|
1038
|
-
lastReportedAt: undefined,
|
|
1039
|
-
createdAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1040
|
-
userCount: undefined,
|
|
1041
|
-
userEmailHashes: undefined,
|
|
1042
|
-
masterAdminEmails: ["admin@acme.com", "owner@acme.com"],
|
|
1043
|
-
});
|
|
1044
|
-
const inactive: EnterpriseLicenseInstance = makeInstance({
|
|
1045
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(calculatedAt, -7),
|
|
1046
|
-
userCount: 2,
|
|
1047
|
-
userEmailHashes: [hashOf("2"), hashOf("3")],
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
1051
|
-
.fn()
|
|
1052
|
-
.mockResolvedValue([active, newlyRegistered, inactive]);
|
|
1053
|
-
|
|
1054
|
-
await callRoute();
|
|
1055
|
-
|
|
1056
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1057
|
-
expect(getResponseBody()).toEqual({
|
|
1058
|
-
currentUserCount: 2,
|
|
1059
|
-
activeInstanceIds: [
|
|
1060
|
-
active.id!.toString(),
|
|
1061
|
-
newlyRegistered.id!.toString(),
|
|
1062
|
-
],
|
|
1063
|
-
masterAdminEmails: ["admin@acme.com", "owner@acme.com"],
|
|
1064
|
-
calculatedAt: calculatedAt.toISOString(),
|
|
1065
|
-
lastUsageReportedAt: calculatedAt.toISOString(),
|
|
1066
|
-
nextInstanceStatusChangeAt: OneUptimeDate.addRemoveDays(
|
|
1067
|
-
calculatedAt,
|
|
1068
|
-
6,
|
|
1069
|
-
).toISOString(),
|
|
1070
|
-
});
|
|
1071
|
-
expect(JSON.stringify(getResponseBody())).not.toContain(hashOf("1"));
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
it("returns zero when every tracked instance is inactive", async () => {
|
|
1075
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
1076
|
-
makeInstance({
|
|
1077
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(calculatedAt, -8),
|
|
1078
|
-
userCount: 50,
|
|
1079
|
-
}),
|
|
1080
|
-
]);
|
|
1081
|
-
|
|
1082
|
-
await callRoute();
|
|
1083
|
-
|
|
1084
|
-
expect(getResponseBody()).toEqual(
|
|
1085
|
-
expect.objectContaining({
|
|
1086
|
-
currentUserCount: 0,
|
|
1087
|
-
activeInstanceIds: [],
|
|
1088
|
-
nextInstanceStatusChangeAt: null,
|
|
1089
|
-
}),
|
|
1090
|
-
);
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
it("uses a fresh legacy count after every modern instance becomes inactive", async () => {
|
|
1094
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1095
|
-
makeLicense({
|
|
1096
|
-
currentUserCount: 17,
|
|
1097
|
-
userCountUpdatedAt: OneUptimeDate.addRemoveDays(calculatedAt, -7),
|
|
1098
|
-
legacyUserCount: 17,
|
|
1099
|
-
legacyUserCountUpdatedAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1100
|
-
}),
|
|
1101
|
-
);
|
|
1102
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
1103
|
-
makeInstance({
|
|
1104
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(calculatedAt, -7),
|
|
1105
|
-
userCount: 50,
|
|
1106
|
-
}),
|
|
1107
|
-
]);
|
|
1108
|
-
|
|
1109
|
-
await callRoute();
|
|
1110
|
-
|
|
1111
|
-
expect(getResponseBody()).toEqual(
|
|
1112
|
-
expect.objectContaining({
|
|
1113
|
-
currentUserCount: 17,
|
|
1114
|
-
activeInstanceIds: [],
|
|
1115
|
-
}),
|
|
1116
|
-
);
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
it("does not preserve an inactive modern aggregate because another instance registered later", async () => {
|
|
1120
|
-
const inactiveAt: Date = OneUptimeDate.addRemoveDays(calculatedAt, -7);
|
|
1121
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1122
|
-
makeLicense({
|
|
1123
|
-
currentUserCount: 50,
|
|
1124
|
-
userCountUpdatedAt: inactiveAt,
|
|
1125
|
-
}),
|
|
1126
|
-
);
|
|
1127
|
-
const newlyRegistered: EnterpriseLicenseInstance = makeInstance({
|
|
1128
|
-
createdAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1129
|
-
lastReportedAt: undefined,
|
|
1130
|
-
userCount: undefined,
|
|
1131
|
-
userEmailHashes: undefined,
|
|
1132
|
-
});
|
|
1133
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
1134
|
-
makeInstance({
|
|
1135
|
-
lastReportedAt: inactiveAt,
|
|
1136
|
-
userCount: 50,
|
|
1137
|
-
}),
|
|
1138
|
-
newlyRegistered,
|
|
1139
|
-
]);
|
|
1140
|
-
|
|
1141
|
-
await callRoute();
|
|
1142
|
-
|
|
1143
|
-
expect(getResponseBody()).toEqual(
|
|
1144
|
-
expect.objectContaining({
|
|
1145
|
-
currentUserCount: 0,
|
|
1146
|
-
activeInstanceIds: [newlyRegistered.id!.toString()],
|
|
1147
|
-
}),
|
|
1148
|
-
);
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
it("schedules an exact refresh when a legacy-only heartbeat expires", async () => {
|
|
1152
|
-
const legacyReportedAt: Date = OneUptimeDate.addRemoveDays(
|
|
1153
|
-
calculatedAt,
|
|
1154
|
-
-1,
|
|
1155
|
-
);
|
|
1156
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1157
|
-
makeLicense({
|
|
1158
|
-
currentUserCount: 17,
|
|
1159
|
-
userCountUpdatedAt: legacyReportedAt,
|
|
1160
|
-
legacyUserCount: 17,
|
|
1161
|
-
legacyUserCountUpdatedAt: legacyReportedAt,
|
|
1162
|
-
}),
|
|
1163
|
-
);
|
|
1164
|
-
|
|
1165
|
-
await callRoute();
|
|
1166
|
-
|
|
1167
|
-
expect(getResponseBody()).toEqual(
|
|
1168
|
-
expect.objectContaining({
|
|
1169
|
-
currentUserCount: 17,
|
|
1170
|
-
activeInstanceIds: [],
|
|
1171
|
-
nextInstanceStatusChangeAt: OneUptimeDate.addRemoveDays(
|
|
1172
|
-
calculatedAt,
|
|
1173
|
-
6,
|
|
1174
|
-
).toISOString(),
|
|
1175
|
-
}),
|
|
1176
|
-
);
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
it("preserves a legacy stored count when no instance rows exist", async () => {
|
|
1180
|
-
EnterpriseLicenseService.findOneById = jest
|
|
1181
|
-
.fn()
|
|
1182
|
-
.mockResolvedValue(makeLicense({ currentUserCount: 17 }));
|
|
1183
|
-
|
|
1184
|
-
await callRoute();
|
|
1185
|
-
|
|
1186
|
-
expect(getResponseBody()).toEqual(
|
|
1187
|
-
expect.objectContaining({
|
|
1188
|
-
currentUserCount: 17,
|
|
1189
|
-
activeInstanceIds: [],
|
|
1190
|
-
nextInstanceStatusChangeAt: OneUptimeDate.addRemoveDays(
|
|
1191
|
-
calculatedAt,
|
|
1192
|
-
7,
|
|
1193
|
-
).toISOString(),
|
|
1194
|
-
}),
|
|
1195
|
-
);
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
it("preserves a legacy stored count when an instance has registered but not reported usage", async () => {
|
|
1199
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1200
|
-
makeLicense({
|
|
1201
|
-
currentUserCount: 17,
|
|
1202
|
-
userCountUpdatedAt: OneUptimeDate.addRemoveDays(calculatedAt, -8),
|
|
1203
|
-
}),
|
|
1204
|
-
);
|
|
1205
|
-
const registeredInstance: EnterpriseLicenseInstance = makeInstance({
|
|
1206
|
-
createdAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1207
|
-
lastReportedAt: undefined,
|
|
1208
|
-
userCount: undefined,
|
|
1209
|
-
userEmailHashes: undefined,
|
|
1210
|
-
});
|
|
1211
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
1212
|
-
.fn()
|
|
1213
|
-
.mockResolvedValue([registeredInstance]);
|
|
1214
|
-
|
|
1215
|
-
await callRoute();
|
|
1216
|
-
|
|
1217
|
-
expect(getResponseBody()).toEqual(
|
|
1218
|
-
expect.objectContaining({
|
|
1219
|
-
currentUserCount: 17,
|
|
1220
|
-
activeInstanceIds: [registeredInstance.id!.toString()],
|
|
1221
|
-
}),
|
|
1222
|
-
);
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
it("drops a legacy count when its report and every registration reach one week old", async () => {
|
|
1226
|
-
const inactiveAt: Date = OneUptimeDate.addRemoveDays(calculatedAt, -7);
|
|
1227
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1228
|
-
makeLicense({
|
|
1229
|
-
currentUserCount: 17,
|
|
1230
|
-
userCountUpdatedAt: inactiveAt,
|
|
1231
|
-
}),
|
|
1232
|
-
);
|
|
1233
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
1234
|
-
makeInstance({
|
|
1235
|
-
createdAt: inactiveAt,
|
|
1236
|
-
lastReportedAt: undefined,
|
|
1237
|
-
userCount: undefined,
|
|
1238
|
-
userEmailHashes: undefined,
|
|
1239
|
-
}),
|
|
1240
|
-
]);
|
|
1241
|
-
|
|
1242
|
-
await callRoute();
|
|
1243
|
-
|
|
1244
|
-
expect(getResponseBody()).toEqual(
|
|
1245
|
-
expect.objectContaining({
|
|
1246
|
-
currentUserCount: 0,
|
|
1247
|
-
activeInstanceIds: [],
|
|
1248
|
-
nextInstanceStatusChangeAt: null,
|
|
1249
|
-
}),
|
|
1250
|
-
);
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
it("does not extend an expired dedicated legacy heartbeat with a newer modern timestamp", async () => {
|
|
1254
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1255
|
-
makeLicense({
|
|
1256
|
-
currentUserCount: 17,
|
|
1257
|
-
userCountUpdatedAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1258
|
-
userCountSource: EnterpriseLicenseUserCountSource.Legacy,
|
|
1259
|
-
legacyUserCount: 17,
|
|
1260
|
-
legacyUserCountUpdatedAt: OneUptimeDate.addRemoveDays(calculatedAt, -7),
|
|
1261
|
-
}),
|
|
1262
|
-
);
|
|
1263
|
-
|
|
1264
|
-
await callRoute();
|
|
1265
|
-
|
|
1266
|
-
expect(getResponseBody()).toEqual(
|
|
1267
|
-
expect.objectContaining({
|
|
1268
|
-
currentUserCount: 0,
|
|
1269
|
-
activeInstanceIds: [],
|
|
1270
|
-
nextInstanceStatusChangeAt: null,
|
|
1271
|
-
}),
|
|
1272
|
-
);
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
it("selects the legacy report timestamp needed for the bounded fallback", async () => {
|
|
1276
|
-
await callRoute();
|
|
1277
|
-
|
|
1278
|
-
expect(EnterpriseLicenseService.findOneById).toHaveBeenCalledWith(
|
|
1279
|
-
expect.objectContaining({
|
|
1280
|
-
select: expect.objectContaining({
|
|
1281
|
-
currentUserCount: true,
|
|
1282
|
-
userCountUpdatedAt: true,
|
|
1283
|
-
userCountSource: true,
|
|
1284
|
-
legacyUserCount: true,
|
|
1285
|
-
legacyUserCountUpdatedAt: true,
|
|
1286
|
-
}),
|
|
1287
|
-
}),
|
|
1288
|
-
);
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
it("serializes the license and instance reads with usage reports", async () => {
|
|
1292
|
-
await callRoute();
|
|
1293
|
-
|
|
1294
|
-
expect(
|
|
1295
|
-
EnterpriseLicenseService.runWithUsageAggregationLock,
|
|
1296
|
-
).toHaveBeenCalledWith({
|
|
1297
|
-
licenseId: LICENSE_ID,
|
|
1298
|
-
fn: expect.any(Function),
|
|
1299
|
-
});
|
|
1300
|
-
expect(
|
|
1301
|
-
(EnterpriseLicenseService.findOneById as unknown as jest.Mock).mock
|
|
1302
|
-
.invocationCallOrder[0],
|
|
1303
|
-
).toBeGreaterThan(
|
|
1304
|
-
(
|
|
1305
|
-
EnterpriseLicenseService.runWithUsageAggregationLock as unknown as jest.Mock
|
|
1306
|
-
).mock.invocationCallOrder[0]!,
|
|
1307
|
-
);
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1310
|
-
it("cannot expose a reported instance with the previous license timestamp", async () => {
|
|
1311
|
-
const previousReportAt: Date = OneUptimeDate.addRemoveDays(
|
|
1312
|
-
calculatedAt,
|
|
1313
|
-
-8,
|
|
1314
|
-
);
|
|
1315
|
-
const licenseState: EnterpriseLicense = makeLicense({
|
|
1316
|
-
currentUserCount: 50,
|
|
1317
|
-
userCountUpdatedAt: previousReportAt,
|
|
1318
|
-
});
|
|
1319
|
-
const instanceState: EnterpriseLicenseInstance = makeInstance({
|
|
1320
|
-
lastReportedAt: previousReportAt,
|
|
1321
|
-
userCount: 50,
|
|
1322
|
-
});
|
|
1323
|
-
let lockTail: Promise<void> = Promise.resolve();
|
|
1324
|
-
let releaseInstanceUpdate: () => void = (): void => {};
|
|
1325
|
-
let markInstanceUpdateStarted: () => void = (): void => {};
|
|
1326
|
-
const instanceUpdateStarted: Promise<void> = new Promise<void>(
|
|
1327
|
-
(resolve: () => void) => {
|
|
1328
|
-
markInstanceUpdateStarted = resolve;
|
|
1329
|
-
},
|
|
1330
|
-
);
|
|
1331
|
-
const instanceUpdateGate: Promise<void> = new Promise<void>(
|
|
1332
|
-
(resolve: () => void) => {
|
|
1333
|
-
releaseInstanceUpdate = resolve;
|
|
1334
|
-
},
|
|
1335
|
-
);
|
|
1336
|
-
|
|
1337
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
1338
|
-
.fn()
|
|
1339
|
-
.mockImplementation(
|
|
1340
|
-
async (data: { fn: () => Promise<unknown> }): Promise<unknown> => {
|
|
1341
|
-
const predecessor: Promise<void> = lockTail;
|
|
1342
|
-
let releaseLock: () => void = (): void => {};
|
|
1343
|
-
lockTail = new Promise<void>((resolve: () => void) => {
|
|
1344
|
-
releaseLock = resolve;
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
await predecessor;
|
|
1348
|
-
try {
|
|
1349
|
-
return await data.fn();
|
|
1350
|
-
} finally {
|
|
1351
|
-
releaseLock();
|
|
1352
|
-
}
|
|
1353
|
-
},
|
|
1354
|
-
);
|
|
1355
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
1356
|
-
.fn()
|
|
1357
|
-
.mockResolvedValue(licenseState);
|
|
1358
|
-
EnterpriseLicenseService.findOneById = jest
|
|
1359
|
-
.fn()
|
|
1360
|
-
.mockImplementation(async (): Promise<EnterpriseLicense> => {
|
|
1361
|
-
return licenseState;
|
|
1362
|
-
});
|
|
1363
|
-
EnterpriseLicenseService.updateOneById = jest
|
|
1364
|
-
.fn()
|
|
1365
|
-
.mockImplementation(
|
|
1366
|
-
async (args: { data: Partial<EnterpriseLicense> }): Promise<number> => {
|
|
1367
|
-
Object.assign(licenseState, args.data);
|
|
1368
|
-
return 1;
|
|
1369
|
-
},
|
|
1370
|
-
);
|
|
1371
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
1372
|
-
.fn()
|
|
1373
|
-
.mockResolvedValue(instanceState);
|
|
1374
|
-
EnterpriseLicenseInstanceService.updateOneById = jest
|
|
1375
|
-
.fn()
|
|
1376
|
-
.mockImplementation(
|
|
1377
|
-
async (args: {
|
|
1378
|
-
data: Partial<EnterpriseLicenseInstance>;
|
|
1379
|
-
}): Promise<number> => {
|
|
1380
|
-
Object.assign(instanceState, args.data);
|
|
1381
|
-
markInstanceUpdateStarted();
|
|
1382
|
-
await instanceUpdateGate;
|
|
1383
|
-
return 1;
|
|
1384
|
-
},
|
|
1385
|
-
);
|
|
1386
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
1387
|
-
.fn()
|
|
1388
|
-
.mockImplementation(
|
|
1389
|
-
async (): Promise<Array<EnterpriseLicenseInstance>> => {
|
|
1390
|
-
return [instanceState];
|
|
1391
|
-
},
|
|
1392
|
-
);
|
|
1393
|
-
|
|
1394
|
-
const reportRequest: OneUptimeRequest = {
|
|
1395
|
-
body: {
|
|
1396
|
-
licenseKey: LICENSE_KEY,
|
|
1397
|
-
userCount: 2,
|
|
1398
|
-
instanceId: "instance-1",
|
|
1399
|
-
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
1400
|
-
},
|
|
1401
|
-
} as unknown as OneUptimeRequest;
|
|
1402
|
-
const reportPromise: Promise<void> = Promise.resolve(
|
|
1403
|
-
mockRouter
|
|
1404
|
-
.match("post", REPORT_ROUTE)
|
|
1405
|
-
.handlerFunction(reportRequest, mockResponse, nextFunction),
|
|
1406
|
-
);
|
|
1407
|
-
|
|
1408
|
-
await instanceUpdateStarted;
|
|
1409
|
-
|
|
1410
|
-
const snapshotPromise: Promise<void> = callRoute();
|
|
1411
|
-
|
|
1412
|
-
releaseInstanceUpdate();
|
|
1413
|
-
await Promise.all([reportPromise, snapshotPromise]);
|
|
1414
|
-
|
|
1415
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1416
|
-
const responseBodies: Array<JSONObject> = (
|
|
1417
|
-
Response.sendJsonObjectResponse as unknown as jest.Mock
|
|
1418
|
-
).mock.calls.map((call: Array<unknown>): JSONObject => {
|
|
1419
|
-
return call[2] as JSONObject;
|
|
1420
|
-
});
|
|
1421
|
-
const snapshotBody: JSONObject = responseBodies.find(
|
|
1422
|
-
(body: JSONObject): boolean => {
|
|
1423
|
-
return Array.isArray(body["activeInstanceIds"]);
|
|
1424
|
-
},
|
|
1425
|
-
)!;
|
|
1426
|
-
|
|
1427
|
-
expect(snapshotBody["currentUserCount"]).toBe(2);
|
|
1428
|
-
expect(snapshotBody["activeInstanceIds"]).toEqual([
|
|
1429
|
-
instanceState.id!.toString(),
|
|
1430
|
-
]);
|
|
1431
|
-
expect(snapshotBody["lastUsageReportedAt"]).toBe(
|
|
1432
|
-
calculatedAt.toISOString(),
|
|
1433
|
-
);
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
it("does not disclose whether an unknown license has instance usage", async () => {
|
|
1437
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(null);
|
|
1438
|
-
|
|
1439
|
-
await callRoute();
|
|
1440
|
-
|
|
1441
|
-
expect(nextFunction).toHaveBeenCalledWith(
|
|
1442
|
-
new BadDataException("Enterprise license not found"),
|
|
1443
|
-
);
|
|
1444
|
-
expect(EnterpriseLicenseInstanceService.findBy).not.toHaveBeenCalled();
|
|
1445
|
-
});
|
|
1446
|
-
});
|
|
1447
|
-
|
|
1448
|
-
describe("EnterpriseLicenseAPI POST /enterprise-license/validate", () => {
|
|
1449
|
-
let mockRequest: OneUptimeRequest;
|
|
1450
|
-
let mockResponse: OneUptimeResponse;
|
|
1451
|
-
let nextFunction: NextFunction;
|
|
1452
|
-
|
|
1453
|
-
beforeEach(() => {
|
|
1454
|
-
jest.clearAllMocks();
|
|
1455
|
-
|
|
1456
|
-
new EnterpriseLicenseAPI();
|
|
1457
|
-
|
|
1458
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
1459
|
-
.fn()
|
|
1460
|
-
.mockResolvedValue(makeLicense());
|
|
1461
|
-
EnterpriseLicenseService.findOneById = jest
|
|
1462
|
-
.fn()
|
|
1463
|
-
.mockResolvedValue(makeLicense());
|
|
1464
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
1465
|
-
.fn()
|
|
1466
|
-
.mockImplementation(
|
|
1467
|
-
async (data: { fn: () => Promise<unknown> }): Promise<unknown> => {
|
|
1468
|
-
return await data.fn();
|
|
1469
|
-
},
|
|
1470
|
-
);
|
|
1471
|
-
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([]);
|
|
1472
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
1473
|
-
.fn()
|
|
1474
|
-
.mockResolvedValue(null);
|
|
1475
|
-
EnterpriseLicenseInstanceService.create = jest
|
|
1476
|
-
.fn()
|
|
1477
|
-
.mockResolvedValue(undefined);
|
|
1478
|
-
EnterpriseLicenseInstanceService.updateOneById = jest
|
|
1479
|
-
.fn()
|
|
1480
|
-
.mockResolvedValue(undefined);
|
|
1481
|
-
EnterpriseLicenseInstanceService.countBy = jest
|
|
1482
|
-
.fn()
|
|
1483
|
-
.mockResolvedValue(new PositiveNumber(0));
|
|
1484
|
-
|
|
1485
|
-
mockRequest = {
|
|
1486
|
-
body: {
|
|
1487
|
-
licenseKey: LICENSE_KEY,
|
|
1488
|
-
instanceId: "instance-1",
|
|
1489
|
-
host: "oneuptime.acme.internal",
|
|
1490
|
-
version: "12.0.19",
|
|
1491
|
-
},
|
|
1492
|
-
} as unknown as OneUptimeRequest;
|
|
1493
|
-
|
|
1494
|
-
mockResponse = {
|
|
1495
|
-
send: jest.fn(),
|
|
1496
|
-
json: jest.fn(),
|
|
1497
|
-
status: jest.fn().mockReturnThis(),
|
|
1498
|
-
} as unknown as OneUptimeResponse;
|
|
1499
|
-
|
|
1500
|
-
nextFunction = jest.fn();
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
afterEach(() => {
|
|
1504
|
-
jest.restoreAllMocks();
|
|
1505
|
-
});
|
|
1506
|
-
|
|
1507
|
-
it("still answers with the same terms after the shared refactor", async () => {
|
|
1508
|
-
const expiresAt: Date = OneUptimeDate.addRemoveDays(
|
|
1509
|
-
OneUptimeDate.getCurrentDate(),
|
|
1510
|
-
90,
|
|
1511
|
-
);
|
|
1512
|
-
|
|
1513
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
1514
|
-
.fn()
|
|
1515
|
-
.mockResolvedValue(makeLicense({ expiresAt: expiresAt }));
|
|
1516
|
-
|
|
1517
|
-
await mockRouter
|
|
1518
|
-
.match("post", VALIDATE_ROUTE)
|
|
1519
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1520
|
-
|
|
1521
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1522
|
-
|
|
1523
|
-
const body: JSONObject = getResponseBody();
|
|
1524
|
-
|
|
1525
|
-
expect(body["companyName"]).toBe("Acme Inc");
|
|
1526
|
-
expect(body["licenseKey"]).toBe(LICENSE_KEY);
|
|
1527
|
-
expect(body["userLimit"]).toBe(150);
|
|
1528
|
-
expect(body["expiresAt"]).toBe(expiresAt.toISOString());
|
|
1529
|
-
expect(body["token"]).toBe("signed.jwt.token");
|
|
1530
|
-
});
|
|
1531
|
-
|
|
1532
|
-
it("registers a new instance without treating validation as a usage heartbeat", async () => {
|
|
1533
|
-
await mockRouter
|
|
1534
|
-
.match("post", VALIDATE_ROUTE)
|
|
1535
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1536
|
-
|
|
1537
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1538
|
-
expect(
|
|
1539
|
-
EnterpriseLicenseInstanceService.updateOneById,
|
|
1540
|
-
).not.toHaveBeenCalled();
|
|
1541
|
-
|
|
1542
|
-
const createArguments: {
|
|
1543
|
-
data: EnterpriseLicenseInstance;
|
|
1544
|
-
} = (EnterpriseLicenseInstanceService.create as unknown as jest.Mock).mock
|
|
1545
|
-
.calls[0]![0] as {
|
|
1546
|
-
data: EnterpriseLicenseInstance;
|
|
1547
|
-
};
|
|
1548
|
-
|
|
1549
|
-
expect(createArguments.data).toEqual(
|
|
1550
|
-
expect.objectContaining({
|
|
1551
|
-
enterpriseLicenseId: LICENSE_ID,
|
|
1552
|
-
instanceId: "instance-1",
|
|
1553
|
-
host: "oneuptime.acme.internal",
|
|
1554
|
-
oneuptimeVersion: "12.0.19",
|
|
1555
|
-
}),
|
|
1556
|
-
);
|
|
1557
|
-
expect(createArguments.data.lastReportedAt).toBeUndefined();
|
|
1558
|
-
expect(createArguments.data.createdAt).toBeUndefined();
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
it("updates instance metadata without refreshing its activity timestamps", async () => {
|
|
1562
|
-
const registeredAt: Date = new Date("2026-08-01T12:00:00.000Z");
|
|
1563
|
-
const lastReportedAt: Date = new Date("2026-08-20T12:00:00.000Z");
|
|
1564
|
-
const existingInstance: EnterpriseLicenseInstance = makeInstance({
|
|
1565
|
-
createdAt: registeredAt,
|
|
1566
|
-
lastReportedAt,
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
1570
|
-
.fn()
|
|
1571
|
-
.mockResolvedValue(existingInstance);
|
|
1572
|
-
|
|
1573
|
-
await mockRouter
|
|
1574
|
-
.match("post", VALIDATE_ROUTE)
|
|
1575
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1576
|
-
|
|
1577
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1578
|
-
expect(EnterpriseLicenseInstanceService.create).not.toHaveBeenCalled();
|
|
1579
|
-
|
|
1580
|
-
const updateArguments: {
|
|
1581
|
-
data: Partial<EnterpriseLicenseInstance>;
|
|
1582
|
-
} = (EnterpriseLicenseInstanceService.updateOneById as unknown as jest.Mock)
|
|
1583
|
-
.mock.calls[0]![0] as {
|
|
1584
|
-
data: Partial<EnterpriseLicenseInstance>;
|
|
1585
|
-
};
|
|
1586
|
-
|
|
1587
|
-
expect(updateArguments.data).toEqual(
|
|
1588
|
-
expect.objectContaining({
|
|
1589
|
-
host: "oneuptime.acme.internal",
|
|
1590
|
-
oneuptimeVersion: "12.0.19",
|
|
1591
|
-
}),
|
|
1592
|
-
);
|
|
1593
|
-
expect(
|
|
1594
|
-
Object.prototype.hasOwnProperty.call(
|
|
1595
|
-
updateArguments.data,
|
|
1596
|
-
"lastReportedAt",
|
|
1597
|
-
),
|
|
1598
|
-
).toBe(false);
|
|
1599
|
-
expect(
|
|
1600
|
-
Object.prototype.hasOwnProperty.call(updateArguments.data, "createdAt"),
|
|
1601
|
-
).toBe(false);
|
|
1602
|
-
expect(existingInstance.lastReportedAt).toBe(lastReportedAt);
|
|
1603
|
-
expect(existingInstance.createdAt).toBe(registeredAt);
|
|
1604
|
-
});
|
|
1605
|
-
|
|
1606
|
-
it("serializes registration and re-reads usage inside the aggregation lock", async () => {
|
|
1607
|
-
const events: Array<string> = [];
|
|
1608
|
-
let isInsideUsageLock: boolean = false;
|
|
1609
|
-
|
|
1610
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
1611
|
-
.fn()
|
|
1612
|
-
.mockImplementation(
|
|
1613
|
-
async (data: { fn: () => Promise<unknown> }): Promise<unknown> => {
|
|
1614
|
-
events.push("lock");
|
|
1615
|
-
isInsideUsageLock = true;
|
|
1616
|
-
|
|
1617
|
-
try {
|
|
1618
|
-
return await data.fn();
|
|
1619
|
-
} finally {
|
|
1620
|
-
isInsideUsageLock = false;
|
|
1621
|
-
}
|
|
1622
|
-
},
|
|
1623
|
-
);
|
|
1624
|
-
EnterpriseLicenseInstanceService.create = jest
|
|
1625
|
-
.fn()
|
|
1626
|
-
.mockImplementation(async (): Promise<void> => {
|
|
1627
|
-
expect(isInsideUsageLock).toBe(true);
|
|
1628
|
-
events.push("register");
|
|
1629
|
-
});
|
|
1630
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
1631
|
-
.fn()
|
|
1632
|
-
.mockImplementation(
|
|
1633
|
-
async (): Promise<Array<EnterpriseLicenseInstance>> => {
|
|
1634
|
-
expect(isInsideUsageLock).toBe(true);
|
|
1635
|
-
events.push("instances");
|
|
1636
|
-
return [];
|
|
1637
|
-
},
|
|
1638
|
-
);
|
|
1639
|
-
/*
|
|
1640
|
-
* Relative to now, like every other date in this file: the legacy usage
|
|
1641
|
-
* fallback only trusts userCountUpdatedAt while it is inside the
|
|
1642
|
-
* InstanceUsageFreshnessInDays window, so a fixed date silently stops
|
|
1643
|
-
* being fresh and the echoed count collapses to 0.
|
|
1644
|
-
*/
|
|
1645
|
-
const userCountUpdatedAt: Date = OneUptimeDate.addRemoveDays(
|
|
1646
|
-
OneUptimeDate.getCurrentDate(),
|
|
1647
|
-
-1,
|
|
1648
|
-
);
|
|
1649
|
-
|
|
1650
|
-
EnterpriseLicenseService.findOneById = jest
|
|
1651
|
-
.fn()
|
|
1652
|
-
.mockImplementation(async (): Promise<EnterpriseLicense> => {
|
|
1653
|
-
expect(isInsideUsageLock).toBe(true);
|
|
1654
|
-
events.push("license");
|
|
1655
|
-
return makeLicense({
|
|
1656
|
-
currentUserCount: 17,
|
|
1657
|
-
userCountUpdatedAt,
|
|
1658
|
-
});
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
|
-
await mockRouter
|
|
1662
|
-
.match("post", VALIDATE_ROUTE)
|
|
1663
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1664
|
-
|
|
1665
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1666
|
-
expect(events).toEqual(["lock", "register", "instances", "license"]);
|
|
1667
|
-
expect(getResponseBody()).toEqual(
|
|
1668
|
-
expect.objectContaining({
|
|
1669
|
-
currentUserCount: 17,
|
|
1670
|
-
userCountUpdatedAt: userCountUpdatedAt.toISOString(),
|
|
1671
|
-
}),
|
|
1672
|
-
);
|
|
1673
|
-
expect(
|
|
1674
|
-
EnterpriseLicenseService.runWithUsageAggregationLock,
|
|
1675
|
-
).toHaveBeenCalledWith({
|
|
1676
|
-
licenseId: LICENSE_ID,
|
|
1677
|
-
fn: expect.any(Function),
|
|
1678
|
-
});
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
it("returns an active-only aggregate with matching per-instance provenance", async () => {
|
|
1682
|
-
const calculatedAt: Date = new Date("2026-09-02T12:00:00.000Z");
|
|
1683
|
-
const inactiveInstance: EnterpriseLicenseInstance = makeInstance({
|
|
1684
|
-
instanceId: "instance-1",
|
|
1685
|
-
userCount: 80,
|
|
1686
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(calculatedAt, -7),
|
|
1687
|
-
});
|
|
1688
|
-
const activeInstance: EnterpriseLicenseInstance = makeInstance({
|
|
1689
|
-
instanceId: "instance-2",
|
|
1690
|
-
userCount: 100,
|
|
1691
|
-
lastReportedAt: OneUptimeDate.addRemoveDays(calculatedAt, -1),
|
|
1692
|
-
});
|
|
1693
|
-
|
|
1694
|
-
jest.spyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(calculatedAt);
|
|
1695
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
1696
|
-
.fn()
|
|
1697
|
-
.mockResolvedValue(inactiveInstance);
|
|
1698
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
1699
|
-
.fn()
|
|
1700
|
-
.mockResolvedValue([inactiveInstance, activeInstance]);
|
|
1701
|
-
EnterpriseLicenseService.findOneById = jest.fn().mockResolvedValue(
|
|
1702
|
-
makeLicense({
|
|
1703
|
-
currentUserCount: 180,
|
|
1704
|
-
userCountUpdatedAt: calculatedAt,
|
|
1705
|
-
userCountSource: EnterpriseLicenseUserCountSource.Instance,
|
|
1706
|
-
}),
|
|
1707
|
-
);
|
|
1708
|
-
|
|
1709
|
-
await mockRouter
|
|
1710
|
-
.match("post", VALIDATE_ROUTE)
|
|
1711
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1712
|
-
|
|
1713
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1714
|
-
expect(getResponseBody()).toEqual(
|
|
1715
|
-
expect.objectContaining({
|
|
1716
|
-
currentUserCount: 100,
|
|
1717
|
-
instances: expect.arrayContaining([
|
|
1718
|
-
expect.objectContaining({
|
|
1719
|
-
instanceId: "instance-1",
|
|
1720
|
-
userCount: 80,
|
|
1721
|
-
isCountedTowardsUsage: false,
|
|
1722
|
-
}),
|
|
1723
|
-
expect.objectContaining({
|
|
1724
|
-
instanceId: "instance-2",
|
|
1725
|
-
userCount: 100,
|
|
1726
|
-
isCountedTowardsUsage: true,
|
|
1727
|
-
}),
|
|
1728
|
-
]),
|
|
1729
|
-
}),
|
|
1730
|
-
);
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
it("keeps rejecting an expired license, unlike the report route", async () => {
|
|
1734
|
-
EnterpriseLicenseService.findOneBy = jest.fn().mockResolvedValue(
|
|
1735
|
-
makeLicense({
|
|
1736
|
-
expiresAt: OneUptimeDate.addRemoveDays(
|
|
1737
|
-
OneUptimeDate.getCurrentDate(),
|
|
1738
|
-
-1,
|
|
1739
|
-
),
|
|
1740
|
-
}),
|
|
1741
|
-
);
|
|
1742
|
-
|
|
1743
|
-
await mockRouter
|
|
1744
|
-
.match("post", VALIDATE_ROUTE)
|
|
1745
|
-
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
1746
|
-
|
|
1747
|
-
expect(nextFunction).toHaveBeenCalledWith(
|
|
1748
|
-
new BadDataException("License key has expired"),
|
|
1749
|
-
);
|
|
1750
|
-
});
|
|
1751
|
-
});
|
|
1752
|
-
|
|
1753
|
-
describe("the contract between the two halves of the sync", () => {
|
|
1754
|
-
let nextFunction: NextFunction;
|
|
1755
|
-
|
|
1756
|
-
beforeEach(() => {
|
|
1757
|
-
jest.clearAllMocks();
|
|
1758
|
-
|
|
1759
|
-
new EnterpriseLicenseAPI();
|
|
1760
|
-
|
|
1761
|
-
EnterpriseLicenseService.findOneBy = jest
|
|
1762
|
-
.fn()
|
|
1763
|
-
.mockResolvedValue(makeLicense());
|
|
1764
|
-
EnterpriseLicenseService.updateOneById = jest
|
|
1765
|
-
.fn()
|
|
1766
|
-
.mockResolvedValue(undefined);
|
|
1767
|
-
EnterpriseLicenseService.runWithUsageAggregationLock = jest
|
|
1768
|
-
.fn()
|
|
1769
|
-
.mockImplementation(
|
|
1770
|
-
async (data: { fn: () => Promise<unknown> }): Promise<unknown> => {
|
|
1771
|
-
return await data.fn();
|
|
1772
|
-
},
|
|
1773
|
-
);
|
|
1774
|
-
EnterpriseLicenseInstanceService.findBy = jest
|
|
1775
|
-
.fn()
|
|
1776
|
-
.mockResolvedValue([makeInstance()]);
|
|
1777
|
-
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
1778
|
-
.fn()
|
|
1779
|
-
.mockResolvedValue(null);
|
|
1780
|
-
EnterpriseLicenseInstanceService.create = jest
|
|
1781
|
-
.fn()
|
|
1782
|
-
.mockResolvedValue(undefined);
|
|
1783
|
-
EnterpriseLicenseInstanceService.countBy = jest
|
|
1784
|
-
.fn()
|
|
1785
|
-
.mockResolvedValue(new PositiveNumber(0));
|
|
1786
|
-
|
|
1787
|
-
nextFunction = jest.fn();
|
|
1788
|
-
});
|
|
1789
|
-
|
|
1790
|
-
afterEach(() => {
|
|
1791
|
-
jest.restoreAllMocks();
|
|
1792
|
-
});
|
|
1793
|
-
|
|
1794
|
-
it("lands every license term when the real response body is fed to the real mapper", async () => {
|
|
1795
|
-
/*
|
|
1796
|
-
* The end of the bug, asserted end to end: the literal object the license
|
|
1797
|
-
* server puts on the wire, through the mapper the installation runs. If
|
|
1798
|
-
* either side renames a field, this fails and nothing else has to.
|
|
1799
|
-
*/
|
|
1800
|
-
await mockRouter.match("post", REPORT_ROUTE).handlerFunction(
|
|
1801
|
-
{
|
|
1802
|
-
body: {
|
|
1803
|
-
licenseKey: LICENSE_KEY,
|
|
1804
|
-
userCount: 2,
|
|
1805
|
-
instanceId: "instance-1",
|
|
1806
|
-
host: "oneuptime.acme.internal",
|
|
1807
|
-
version: "12.0.19",
|
|
1808
|
-
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
1809
|
-
masterAdminEmails: ["admin@acme.com"],
|
|
1810
|
-
},
|
|
1811
|
-
} as unknown as OneUptimeRequest,
|
|
1812
|
-
{
|
|
1813
|
-
send: jest.fn(),
|
|
1814
|
-
json: jest.fn(),
|
|
1815
|
-
status: jest.fn().mockReturnThis(),
|
|
1816
|
-
} as unknown as OneUptimeResponse,
|
|
1817
|
-
nextFunction,
|
|
1818
|
-
);
|
|
1819
|
-
|
|
1820
|
-
expect(nextFunction).not.toHaveBeenCalled();
|
|
1821
|
-
|
|
1822
|
-
const wireBody: JSONObject = getResponseBody();
|
|
1823
|
-
|
|
1824
|
-
const result: EnterpriseLicenseSyncResult =
|
|
1825
|
-
EnterpriseLicenseSyncUtil.getGlobalConfigUpdateFromLicenseResponse({
|
|
1826
|
-
payload: wireBody,
|
|
1827
|
-
reportedAt: OneUptimeDate.getCurrentDate(),
|
|
1828
|
-
});
|
|
1829
|
-
|
|
1830
|
-
expect(result.warnings).toEqual([]);
|
|
1831
|
-
expect(result.updateData.enterpriseLicenseUserLimit).toBe(150);
|
|
1832
|
-
expect(result.updateData.enterpriseLicenseCurrentUserCount).toBe(2);
|
|
1833
|
-
expect(result.updateData.enterpriseCompanyName).toBe("Acme Inc");
|
|
1834
|
-
expect(result.updateData.enterpriseLicenseIsEvaluation).toBe(false);
|
|
1835
|
-
expect(result.updateData.enterpriseLicenseToken).toBe("signed.jwt.token");
|
|
1836
|
-
expect(result.updateData.enterpriseLicenseExpiresAt).toBeInstanceOf(Date);
|
|
1837
|
-
expect(result.updateData.enterpriseLicenseInstances).toHaveLength(1);
|
|
1838
|
-
expect(
|
|
1839
|
-
(
|
|
1840
|
-
result.updateData.enterpriseLicenseInstances as
|
|
1841
|
-
| Array<EnterpriseLicenseInstanceSummary>
|
|
1842
|
-
| undefined
|
|
1843
|
-
)?.[0]?.isCountedTowardsUsage,
|
|
1844
|
-
).toBe(true);
|
|
1845
|
-
|
|
1846
|
-
// The installation never adopts a key handed to it by a response.
|
|
1847
|
-
expect(Object.keys(result.updateData)).not.toContain(
|
|
1848
|
-
"enterpriseLicenseKey",
|
|
1849
|
-
);
|
|
1850
|
-
});
|
|
1851
|
-
});
|