@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.
Files changed (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -0,0 +1,1586 @@
1
+ import EditionLabel, {
2
+ GRACE_ENFORCEMENT_SUMMARY,
3
+ LICENSE_LAPSED_STATE,
4
+ LICENSE_LAPSE_CONSEQUENCES,
5
+ TRIAL_ENFORCEMENT_SUMMARY,
6
+ } from "../../../UI/Components/EditionLabel/EditionLabel";
7
+ import {
8
+ LicenseManagerDialogParts,
9
+ LicenseManagerProps,
10
+ } from "../../../UI/Components/EditionLabel/LicenseManager";
11
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
12
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
13
+ import { JSONObject } from "../../../Types/JSON";
14
+ import {
15
+ ENTERPRISE_LICENSE_GRACE_PERIOD_IN_DAYS,
16
+ ENTERPRISE_LICENSE_TRIAL_PERIOD_IN_DAYS,
17
+ } from "../../../Types/EnterpriseLicense/EnterpriseLicensePeriods";
18
+ import {
19
+ LAPSED_STATE_PHRASES,
20
+ LAPSE_WARNING_PHRASES,
21
+ RETIRED_NOTICE_EXAMPLES,
22
+ lapsedStateProblems,
23
+ lapseWarningProblems,
24
+ } from "./EditionLabelLapseCopy";
25
+ import "@testing-library/jest-dom";
26
+ import {
27
+ act,
28
+ fireEvent,
29
+ render,
30
+ screen,
31
+ waitFor,
32
+ } from "@testing-library/react";
33
+ import React from "react";
34
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
35
+
36
+ /*
37
+ * The core half of the edition dialog: what EditionLabel shows on its own,
38
+ * with no license manager - the login page, and any build without the
39
+ * Enterprise plugin. Everything that changes the license (activation, offline
40
+ * activation, refresh, the seat and instance cards) is the Enterprise
41
+ * plugin's LicenseManager now; ee/Tests/UI/License runs this suite's original
42
+ * tests, all of them, against EditionLabel with that manager. This one must
43
+ * pass with ee/ deleted.
44
+ *
45
+ * What the edition pill and dialog say about the license, now that the
46
+ * Enterprise license client reports a status rather than a yes/no:
47
+ *
48
+ * valid as before;
49
+ * grace either an expired license's grace period (renew) or an unlicensed
50
+ * installation's trial counted from its first run (add a license) -
51
+ * the two must never be confused;
52
+ * expired, missing, invalid - with the lapse said plainly: single sign-on,
53
+ * SCIM and audit logging are off, "Require SSO" is not enforced
54
+ * (users sign in with their password), configuration is
55
+ * read-only, and everything resumes when a license is added. The
56
+ * trial and grace notices warn about exactly that beforehand
57
+ * (EditionLabelLapseCopy.ts);
58
+ * the Community Edition image running with IS_ENTERPRISE_EDITION set, told to
59
+ * a master admin;
60
+ * and what EditionLabel hands a license manager, and where its parts land.
61
+ */
62
+
63
+ let isEnterpriseEdition: boolean = true;
64
+ let billingEnabled: boolean = false;
65
+ let isMasterAdmin: boolean = true;
66
+
67
+ jest.mock("../../../UI/Config", () => {
68
+ const actualConfig: Record<string, unknown> = jest.requireActual(
69
+ "../../../UI/Config",
70
+ ) as Record<string, unknown>;
71
+
72
+ const mockedConfig: Record<string, unknown> = { ...actualConfig };
73
+
74
+ Object.defineProperty(mockedConfig, "IS_ENTERPRISE_EDITION", {
75
+ get: (): boolean => {
76
+ return isEnterpriseEdition;
77
+ },
78
+ });
79
+
80
+ Object.defineProperty(mockedConfig, "BILLING_ENABLED", {
81
+ get: (): boolean => {
82
+ return billingEnabled;
83
+ },
84
+ });
85
+
86
+ return mockedConfig;
87
+ });
88
+
89
+ jest.mock("../../../UI/Utils/User", () => {
90
+ return {
91
+ __esModule: true,
92
+ default: {
93
+ isMasterAdmin: (): boolean => {
94
+ return isMasterAdmin;
95
+ },
96
+ /*
97
+ * EditionLabel asks whether anybody is signed in before it fetches the
98
+ * license, to add `signedIn=true` (see EditionLabelLicenseRefresh).
99
+ * These are signed-in dashboard screens.
100
+ */
101
+ isLoggedIn: (): boolean => {
102
+ return true;
103
+ },
104
+ },
105
+ };
106
+ });
107
+
108
+ interface FetchCall {
109
+ method: string;
110
+ url: string;
111
+ data: JSONObject | undefined;
112
+ }
113
+
114
+ const fetchCalls: Array<FetchCall> = [];
115
+
116
+ type FetchResponder = (
117
+ call: FetchCall,
118
+ ) => HTTPResponse<JSONObject> | HTTPErrorResponse;
119
+
120
+ let respond: FetchResponder;
121
+
122
+ jest.mock("../../../UI/Utils/API/API", () => {
123
+ return {
124
+ __esModule: true,
125
+ default: {
126
+ fetch: (options: {
127
+ method: { toString: () => string };
128
+ url: { toString: () => string };
129
+ data?: JSONObject | undefined;
130
+ }): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> => {
131
+ const call: FetchCall = {
132
+ method: options.method.toString(),
133
+ url: options.url.toString(),
134
+ data: options.data,
135
+ };
136
+
137
+ fetchCalls.push(call);
138
+
139
+ return Promise.resolve(respond(call));
140
+ },
141
+ getFriendlyMessage: (err: unknown): string => {
142
+ if (err instanceof HTTPErrorResponse) {
143
+ return err.message;
144
+ }
145
+
146
+ return String(err);
147
+ },
148
+ },
149
+ };
150
+ });
151
+
152
+ const DAY_IN_MS: number = 24 * 60 * 60 * 1000;
153
+ const MISMATCH_FLAG: string = "ENTERPRISE_EDITION_REQUESTED_BUT_NOT_LOADED";
154
+
155
+ const inDays: (days: number) => string = (days: number): string => {
156
+ return new Date(Date.now() + days * DAY_IN_MS).toISOString();
157
+ };
158
+
159
+ type PayloadFunction = (overrides?: Record<string, unknown>) => JSONObject;
160
+
161
+ // What GET /global-config/license gives a master admin for a valid license.
162
+ const adminPayload: PayloadFunction = (
163
+ overrides?: Record<string, unknown>,
164
+ ): JSONObject => {
165
+ return {
166
+ edition: "enterprise",
167
+ status: "valid",
168
+ verification: "verified",
169
+ graceReason: null,
170
+ graceEndsAt: null,
171
+ licenseValid: true,
172
+ companyName: "Acme Inc",
173
+ expiresAt: inDays(200),
174
+ isEvaluation: false,
175
+ isEvaluationLicense: false,
176
+ message: null,
177
+ licenseKey: "acme-license-key",
178
+ token: "signed.license.token",
179
+ activationMode: "online",
180
+ userLimit: 50,
181
+ currentUserCount: 10,
182
+ userCountUpdatedAt: "2026-01-01T00:00:00.000Z",
183
+ instances: [],
184
+ instanceId: "instance-1",
185
+ currentVersion: "13.0.0",
186
+ latestVersion: "13.0.0",
187
+ latestVersionPublishedAt: null,
188
+ latestVersionCheckedAt: null,
189
+ isUpdateAvailable: false,
190
+ isUpdateCheckDisabled: false,
191
+ isSeatLimitEnforced: true,
192
+ seatsInUse: 10,
193
+ seatsRemaining: 40,
194
+ canAddMoreUsers: true,
195
+ ...(overrides || {}),
196
+ };
197
+ };
198
+
199
+ // What anybody else gets.
200
+ const publicPayload: PayloadFunction = (
201
+ overrides?: Record<string, unknown>,
202
+ ): JSONObject => {
203
+ return {
204
+ edition: "enterprise",
205
+ status: "valid",
206
+ verification: "verified",
207
+ graceReason: null,
208
+ graceEndsAt: null,
209
+ licenseValid: true,
210
+ companyName: "Acme Inc",
211
+ expiresAt: inDays(200),
212
+ isEvaluation: false,
213
+ isEvaluationLicense: false,
214
+ ...(overrides || {}),
215
+ };
216
+ };
217
+
218
+ const respondWith: (payload: JSONObject) => void = (
219
+ payload: JSONObject,
220
+ ): void => {
221
+ respond = (): HTTPResponse<JSONObject> => {
222
+ return new HTTPResponse<JSONObject>(200, payload, {});
223
+ };
224
+ };
225
+
226
+ const renderPill: () => Promise<void> = async (): Promise<void> => {
227
+ render(<EditionLabel />);
228
+
229
+ await waitFor(() => {
230
+ expect(fetchCalls.length).toBeGreaterThan(0);
231
+ });
232
+ };
233
+
234
+ const openDialog: () => Promise<void> = async (): Promise<void> => {
235
+ await renderPill();
236
+ fireEvent.click(screen.getByRole("button", { name: /Enterprise Edition/i }));
237
+ };
238
+
239
+ const pill: () => HTMLElement = (): HTMLElement => {
240
+ return screen.getByRole("button", { name: /Edition/ });
241
+ };
242
+
243
+ beforeEach(() => {
244
+ fetchCalls.length = 0;
245
+ isEnterpriseEdition = true;
246
+ billingEnabled = false;
247
+ isMasterAdmin = true;
248
+ delete process.env[MISMATCH_FLAG];
249
+ respondWith(adminPayload());
250
+ });
251
+
252
+ afterEach(() => {
253
+ delete process.env[MISMATCH_FLAG];
254
+ });
255
+
256
+ describe("EditionLabel - a valid license", () => {
257
+ it("reads plainly as the Enterprise Edition", async () => {
258
+ await renderPill();
259
+
260
+ await waitFor(() => {
261
+ expect(pill()).toHaveAccessibleName("Enterprise Edition, View details");
262
+ });
263
+ });
264
+
265
+ it("shows no status notice", async () => {
266
+ await openDialog();
267
+
268
+ expect(await screen.findByText("Licensed to")).toBeInTheDocument();
269
+ expect(
270
+ screen.queryByTestId("enterprise-license-required-notice"),
271
+ ).not.toBeInTheDocument();
272
+ expect(
273
+ screen.queryByTestId("enterprise-license-grace-notice"),
274
+ ).not.toBeInTheDocument();
275
+ expect(
276
+ screen.queryByTestId("enterprise-license-trial-notice"),
277
+ ).not.toBeInTheDocument();
278
+ });
279
+ });
280
+
281
+ describe("EditionLabel - an expired license in its grace period", () => {
282
+ const graceEndsAt: string = inDays(5);
283
+
284
+ beforeEach(() => {
285
+ respondWith(
286
+ adminPayload({
287
+ status: "grace",
288
+ graceReason: "expired",
289
+ graceEndsAt,
290
+ expiresAt: inDays(-25),
291
+ }),
292
+ );
293
+ });
294
+
295
+ it("says the license expired and asks for a renewal on the pill", async () => {
296
+ await renderPill();
297
+
298
+ await waitFor(() => {
299
+ expect(pill()).toHaveAccessibleName(
300
+ "Enterprise Edition (License Expired, Grace Period), Renew license",
301
+ );
302
+ });
303
+ });
304
+
305
+ it("tells when the grace period ends, and what happens then", async () => {
306
+ await openDialog();
307
+
308
+ const notice: HTMLElement = await screen.findByTestId(
309
+ "enterprise-license-grace-notice",
310
+ );
311
+
312
+ expect(notice).toHaveTextContent(
313
+ new Date(graceEndsAt).toLocaleDateString(),
314
+ );
315
+ expect(notice).toHaveTextContent("5 days left");
316
+ expect(notice).toHaveTextContent(
317
+ "Every enterprise feature keeps working until the grace period ends",
318
+ );
319
+ // What stops when it ends: SSO, SCIM and audit logging, not just configuration.
320
+ expect(lapseWarningProblems(notice.textContent)).toEqual([]);
321
+ });
322
+
323
+ /*
324
+ * The seats and the refresh button that sit beside these belong to the
325
+ * license manager (ee/Tests/UI/License/LicenseManagerStatus pins them).
326
+ */
327
+ it("still counts as licensed: the details and the grace badge stay", async () => {
328
+ await openDialog();
329
+
330
+ expect(await screen.findByText("Licensed to")).toBeInTheDocument();
331
+ expect(screen.getByText("Grace period")).toBeInTheDocument();
332
+ });
333
+
334
+ it("calls it a grace period, never a trial", async () => {
335
+ await openDialog();
336
+
337
+ const notice: HTMLElement = await screen.findByTestId(
338
+ "enterprise-license-grace-notice",
339
+ );
340
+
341
+ expect(notice).toHaveTextContent(
342
+ "Without a valid license (after the 30-day grace period), single sign-on (SAML and OIDC) stops",
343
+ );
344
+ expect(notice).not.toHaveTextContent(/trial/i);
345
+ });
346
+
347
+ it("does not call an expired license a trial", async () => {
348
+ await openDialog();
349
+
350
+ await screen.findByTestId("enterprise-license-grace-notice");
351
+
352
+ expect(screen.queryByText(/trial/i)).not.toBeInTheDocument();
353
+ });
354
+
355
+ it("asks somebody who is not a master admin to find one", async () => {
356
+ isMasterAdmin = false;
357
+ respondWith(
358
+ publicPayload({
359
+ status: "grace",
360
+ graceReason: "expired",
361
+ graceEndsAt,
362
+ expiresAt: inDays(-25),
363
+ }),
364
+ );
365
+
366
+ await openDialog();
367
+
368
+ expect(
369
+ await screen.findByText(
370
+ "Ask a master admin of this installation to renew the license.",
371
+ ),
372
+ ).toBeInTheDocument();
373
+ });
374
+ });
375
+
376
+ describe("EditionLabel - an unlicensed installation's trial", () => {
377
+ const graceEndsAt: string = inDays(10);
378
+
379
+ beforeEach(() => {
380
+ respondWith(
381
+ adminPayload({
382
+ status: "grace",
383
+ graceReason: "unlicensed",
384
+ verification: "none",
385
+ graceEndsAt,
386
+ licenseValid: true,
387
+ companyName: null,
388
+ expiresAt: null,
389
+ licenseKey: null,
390
+ token: null,
391
+ activationMode: null,
392
+ userLimit: null,
393
+ isSeatLimitEnforced: false,
394
+ seatsInUse: null,
395
+ seatsRemaining: null,
396
+ }),
397
+ );
398
+ });
399
+
400
+ it("shows the days left on the pill", async () => {
401
+ await renderPill();
402
+
403
+ await waitFor(() => {
404
+ expect(pill()).toHaveAccessibleName(
405
+ "Enterprise Edition (Trial, 10 days left), Add license",
406
+ );
407
+ });
408
+ });
409
+
410
+ it("explains the trial and what happens after it", async () => {
411
+ await openDialog();
412
+
413
+ const notice: HTMLElement = await screen.findByTestId(
414
+ "enterprise-license-trial-notice",
415
+ );
416
+
417
+ expect(notice).toHaveTextContent("Enterprise Edition trial: 10 days left");
418
+ expect(notice).toHaveTextContent("No Enterprise license is installed");
419
+ expect(notice).toHaveTextContent(
420
+ new Date(graceEndsAt).toLocaleDateString(),
421
+ );
422
+ // What stops when it ends: SSO, SCIM and audit logging, not just configuration.
423
+ expect(lapseWarningProblems(notice.textContent)).toEqual([]);
424
+ });
425
+
426
+ /*
427
+ * The first 14 days of an unlicensed install are its trial; only a license
428
+ * that lapsed has a grace period. The notice used to call both "the 14-day
429
+ * grace period".
430
+ */
431
+ it("calls the first 14 days a trial, not a grace period", async () => {
432
+ await openDialog();
433
+
434
+ const notice: HTMLElement = await screen.findByTestId(
435
+ "enterprise-license-trial-notice",
436
+ );
437
+
438
+ expect(notice).toHaveTextContent(
439
+ "Without a valid license (after the 14-day trial), single sign-on (SAML and OIDC) stops",
440
+ );
441
+ expect(notice).not.toHaveTextContent(/grace/i);
442
+ });
443
+
444
+ it("shows no empty license details for a license that does not exist", async () => {
445
+ await openDialog();
446
+
447
+ await screen.findByTestId("enterprise-license-trial-notice");
448
+
449
+ expect(screen.queryByText("Licensed to")).not.toBeInTheDocument();
450
+ expect(screen.queryByText("Licensed seats")).not.toBeInTheDocument();
451
+ expect(screen.queryByText(/renew/i)).not.toBeInTheDocument();
452
+ });
453
+
454
+ it("says one day, not one days", async () => {
455
+ respondWith(
456
+ adminPayload({
457
+ status: "grace",
458
+ graceReason: "unlicensed",
459
+ verification: "none",
460
+ graceEndsAt: new Date(Date.now() + DAY_IN_MS / 2).toISOString(),
461
+ }),
462
+ );
463
+
464
+ await renderPill();
465
+
466
+ await waitFor(() => {
467
+ expect(pill()).toHaveAccessibleName(
468
+ "Enterprise Edition (Trial, 1 day left), Add license",
469
+ );
470
+ });
471
+ });
472
+
473
+ it("points somebody who is not a master admin at one", async () => {
474
+ isMasterAdmin = false;
475
+ respondWith(
476
+ publicPayload({
477
+ status: "grace",
478
+ graceReason: "unlicensed",
479
+ verification: "none",
480
+ graceEndsAt,
481
+ companyName: null,
482
+ expiresAt: null,
483
+ }),
484
+ );
485
+
486
+ await openDialog();
487
+
488
+ expect(
489
+ await screen.findByText(
490
+ "Ask a master admin of this installation to add a license.",
491
+ ),
492
+ ).toBeInTheDocument();
493
+ expect(
494
+ screen.queryByPlaceholderText("Enter your enterprise license key"),
495
+ ).not.toBeInTheDocument();
496
+ });
497
+ });
498
+
499
+ describe("EditionLabel - no usable license", () => {
500
+ it.each([
501
+ [
502
+ "expired",
503
+ "Enterprise Edition (License Expired), Renew license",
504
+ "License expired",
505
+ ],
506
+ [
507
+ "missing",
508
+ "Enterprise Edition (License Required), Validate license",
509
+ "No valid license",
510
+ ],
511
+ [
512
+ "invalid",
513
+ "Enterprise Edition (License Invalid), Fix license",
514
+ "License not valid",
515
+ ],
516
+ ])(
517
+ "a %s license reads as such on the pill and in the dialog",
518
+ async (status: string, pillName: string, noticeTitle: string) => {
519
+ respondWith(
520
+ adminPayload({
521
+ status,
522
+ licenseValid: false,
523
+ verification: status === "missing" ? "none" : "verified",
524
+ }),
525
+ );
526
+
527
+ await renderPill();
528
+
529
+ await waitFor(() => {
530
+ expect(pill()).toHaveAccessibleName(pillName);
531
+ });
532
+
533
+ fireEvent.click(pill());
534
+
535
+ const notice: HTMLElement = await screen.findByTestId(
536
+ "enterprise-license-required-notice",
537
+ );
538
+
539
+ expect(notice).toHaveTextContent(noticeTitle);
540
+ /*
541
+ * The lapse, said plainly: single sign-on, SCIM and audit logging are
542
+ * off, "Require SSO" is not enforced, configuration is read-only, it
543
+ * all resumes with a license, and core monitoring is not touched.
544
+ */
545
+ expect(lapsedStateProblems(notice.textContent)).toEqual([]);
546
+ },
547
+ );
548
+
549
+ it("shows a master admin why the license is not valid", async () => {
550
+ respondWith(
551
+ adminPayload({
552
+ status: "invalid",
553
+ licenseValid: false,
554
+ message: "The license is bound to a different OneUptime instance.",
555
+ }),
556
+ );
557
+
558
+ await openDialog();
559
+
560
+ expect(
561
+ await screen.findByTestId("enterprise-license-required-notice"),
562
+ ).toHaveTextContent(
563
+ "The license is bound to a different OneUptime instance.",
564
+ );
565
+ });
566
+
567
+ /*
568
+ * The old copy promised that validating a key would "turn these on
569
+ * immediately" - the list is services (support, indemnification) as much as
570
+ * features, so it still must not. It then said "Nothing you already
571
+ * configured stops working without one", which stopped being true when
572
+ * single sign-on, SCIM and audit logging began to stop with the license.
573
+ */
574
+ it("says what a license keeps running, and no longer that nothing stops without one", async () => {
575
+ respondWith(adminPayload({ status: "missing", licenseValid: false }));
576
+
577
+ await openDialog();
578
+
579
+ await screen.findByText("What your license unlocks");
580
+
581
+ expect(
582
+ screen.queryByText(/turn these on immediately/),
583
+ ).not.toBeInTheDocument();
584
+ // Without a manager nobody here can add the license, so it names who can.
585
+ expect(
586
+ screen.getByText(
587
+ /A master admin can add the license to keep single sign-on, SCIM provisioning and audit logging running/,
588
+ ),
589
+ ).toBeInTheDocument();
590
+ expect(
591
+ screen.queryByText(/Nothing you already configured stops working/),
592
+ ).not.toBeInTheDocument();
593
+ });
594
+
595
+ it("asks for a license to turn single sign-on, SCIM and audit logging back on", async () => {
596
+ respondWith(adminPayload({ status: "expired", licenseValid: false }));
597
+
598
+ await openDialog();
599
+
600
+ expect(
601
+ await screen.findByText(
602
+ "Add a valid license to turn single sign-on, SCIM and audit logging back on and make enterprise configuration editable.",
603
+ ),
604
+ ).toBeInTheDocument();
605
+ });
606
+ });
607
+
608
+ describe("EditionLabel - an unverified legacy license", () => {
609
+ /*
610
+ * The notice tells a master admin that refreshing the license fixes it,
611
+ * which takes the license manager: with it, a master admin is told
612
+ * (ee/Tests/UI/License/LicenseManagerStatus); without it, nobody is.
613
+ */
614
+ it("is not raised where the license cannot be refreshed", async () => {
615
+ respondWith(adminPayload({ verification: "unverified" }));
616
+
617
+ await openDialog();
618
+
619
+ await screen.findByText("Licensed to");
620
+
621
+ expect(
622
+ screen.queryByTestId("enterprise-license-unverified-notice"),
623
+ ).not.toBeInTheDocument();
624
+ });
625
+
626
+ it("does not trouble anybody else with it", async () => {
627
+ isMasterAdmin = false;
628
+ respondWith(publicPayload({ verification: "unverified" }));
629
+
630
+ await openDialog();
631
+
632
+ await screen.findByText("Licensed to");
633
+
634
+ expect(
635
+ screen.queryByTestId("enterprise-license-unverified-notice"),
636
+ ).not.toBeInTheDocument();
637
+ });
638
+
639
+ it("still reads as a valid license", async () => {
640
+ respondWith(adminPayload({ verification: "unverified" }));
641
+
642
+ await renderPill();
643
+
644
+ await waitFor(() => {
645
+ expect(pill()).toHaveAccessibleName("Enterprise Edition, View details");
646
+ });
647
+ });
648
+ });
649
+
650
+ describe("EditionLabel - the Community Edition", () => {
651
+ beforeEach(() => {
652
+ isEnterpriseEdition = false;
653
+ respondWith(
654
+ adminPayload({
655
+ edition: "community",
656
+ status: null,
657
+ verification: null,
658
+ licenseValid: false,
659
+ licenseKey: null,
660
+ token: null,
661
+ }),
662
+ );
663
+ });
664
+
665
+ it("does not ask about a license on page load", async () => {
666
+ render(<EditionLabel />);
667
+
668
+ expect(
669
+ screen.getByRole("button", {
670
+ name: "Community Edition, Learn more",
671
+ }),
672
+ ).toBeInTheDocument();
673
+ expect(fetchCalls).toHaveLength(0);
674
+ });
675
+
676
+ it("keeps the comparison and the version card", async () => {
677
+ render(<EditionLabel />);
678
+ fireEvent.click(screen.getByRole("button", { name: /Community Edition/ }));
679
+
680
+ expect(
681
+ await screen.findByText(/You are running the Community Edition/),
682
+ ).toBeInTheDocument();
683
+ expect(await screen.findByText("This installation")).toBeInTheDocument();
684
+ expect(screen.getByText("v13.0.0")).toBeInTheDocument();
685
+ });
686
+
687
+ /*
688
+ * A leftover IS_ENTERPRISE_EDITION=true on the Community image: the server
689
+ * tells the frontend through env.js. Only a master admin can switch the
690
+ * image, so only a master admin is told.
691
+ */
692
+ it("tells a master admin to switch to the enterprise image", async () => {
693
+ process.env[MISMATCH_FLAG] = "true";
694
+
695
+ render(<EditionLabel />);
696
+
697
+ const button: HTMLElement = screen.getByRole("button", {
698
+ name: "Community Edition, Action needed",
699
+ });
700
+
701
+ fireEvent.click(button);
702
+
703
+ expect(
704
+ await screen.findByTestId("enterprise-edition-image-mismatch"),
705
+ ).toHaveTextContent(
706
+ "IS_ENTERPRISE_EDITION is set but this is the Community Edition image — switch to the enterprise image",
707
+ );
708
+ });
709
+
710
+ it("does not tell anybody else", async () => {
711
+ process.env[MISMATCH_FLAG] = "true";
712
+ isMasterAdmin = false;
713
+
714
+ render(<EditionLabel />);
715
+ fireEvent.click(
716
+ screen.getByRole("button", { name: "Community Edition, Learn more" }),
717
+ );
718
+
719
+ await screen.findByText(/You are running the Community Edition/);
720
+
721
+ expect(
722
+ screen.queryByTestId("enterprise-edition-image-mismatch"),
723
+ ).not.toBeInTheDocument();
724
+ });
725
+
726
+ it("says nothing about the image when it was not asked for", async () => {
727
+ render(<EditionLabel />);
728
+ fireEvent.click(
729
+ screen.getByRole("button", { name: "Community Edition, Learn more" }),
730
+ );
731
+
732
+ await screen.findByText(/You are running the Community Edition/);
733
+
734
+ expect(
735
+ screen.queryByTestId("enterprise-edition-image-mismatch"),
736
+ ).not.toBeInTheDocument();
737
+ });
738
+ });
739
+
740
+ describe("EditionLabel - oneuptime.com", () => {
741
+ it("renders nothing and asks nothing with billing enabled", async () => {
742
+ billingEnabled = true;
743
+
744
+ const { container } = render(<EditionLabel />);
745
+
746
+ await act(async () => {
747
+ await Promise.resolve();
748
+ });
749
+
750
+ expect(container).toBeEmptyDOMElement();
751
+ expect(fetchCalls).toHaveLength(0);
752
+ });
753
+
754
+ /*
755
+ * Every hook runs whatever the edition: the early return for billing sits
756
+ * after them, so a re-render that changes the answer cannot change the
757
+ * number of hooks React sees.
758
+ */
759
+ it("keeps React's hook order when the billing answer changes between renders", async () => {
760
+ billingEnabled = true;
761
+
762
+ const { rerender, container } = render(<EditionLabel />);
763
+
764
+ billingEnabled = false;
765
+ rerender(<EditionLabel />);
766
+
767
+ await waitFor(() => {
768
+ expect(container).not.toBeEmptyDOMElement();
769
+ });
770
+ });
771
+ });
772
+
773
+ const LICENSE_KEY_PLACEHOLDER: string = "Enter your enterprise license key";
774
+
775
+ const postCalls: () => Array<FetchCall> = (): Array<FetchCall> => {
776
+ return fetchCalls.filter((call: FetchCall): boolean => {
777
+ return call.method !== "GET";
778
+ });
779
+ };
780
+
781
+ // Nothing that changes the license is on screen.
782
+ const expectNoLicenseControls: () => void = (): void => {
783
+ expect(
784
+ screen.queryByPlaceholderText(LICENSE_KEY_PLACEHOLDER),
785
+ ).not.toBeInTheDocument();
786
+ expect(
787
+ screen.queryByTestId("enterprise-license-token-input"),
788
+ ).not.toBeInTheDocument();
789
+ expect(
790
+ screen.queryByTestId("switch-license-activation-mode"),
791
+ ).not.toBeInTheDocument();
792
+ expect(
793
+ screen.queryByTestId("refresh-enterprise-license"),
794
+ ).not.toBeInTheDocument();
795
+ expect(screen.queryByText("Change license key")).not.toBeInTheDocument();
796
+ expect(screen.queryByText("Replace license")).not.toBeInTheDocument();
797
+ expect(
798
+ screen.queryByRole("button", { name: /Validate License|Activate License/ }),
799
+ ).not.toBeInTheDocument();
800
+ expect(screen.queryByText("Licensed seats")).not.toBeInTheDocument();
801
+ expect(
802
+ screen.queryByText("Instances on this license"),
803
+ ).not.toBeInTheDocument();
804
+ };
805
+
806
+ /*
807
+ * The login page, and any Dashboard or Admin Dashboard built without the
808
+ * Enterprise plugin: EditionLabel on its own reads the license and never
809
+ * writes it, and tells nobody - not even a master admin - to manage it from
810
+ * a dialog that cannot.
811
+ */
812
+ describe("EditionLabel - without a license manager", () => {
813
+ it.each([
814
+ ["no license", { status: "missing", licenseValid: false }],
815
+ [
816
+ "the trial",
817
+ {
818
+ status: "grace",
819
+ graceReason: "unlicensed",
820
+ graceEndsAt: inDays(10),
821
+ licenseKey: null,
822
+ },
823
+ ],
824
+ [
825
+ "an expired license in its grace period",
826
+ { status: "grace", graceReason: "expired", graceEndsAt: inDays(5) },
827
+ ],
828
+ ["a valid license", {}],
829
+ ])(
830
+ "offers no license controls and writes nothing for %s",
831
+ async (_name: string, overrides: Record<string, unknown>) => {
832
+ respondWith(adminPayload(overrides));
833
+
834
+ await openDialog();
835
+
836
+ await waitFor(() => {
837
+ expect(screen.getByText("This installation")).toBeInTheDocument();
838
+ });
839
+
840
+ expectNoLicenseControls();
841
+ expect(postCalls()).toEqual([]);
842
+ },
843
+ );
844
+
845
+ it("points even a master admin at somebody else during the trial", async () => {
846
+ respondWith(
847
+ adminPayload({
848
+ status: "grace",
849
+ graceReason: "unlicensed",
850
+ /*
851
+ * No license at all, which is what the server sends for this trial:
852
+ * no token, and verification "none" because there was nothing to
853
+ * judge. An install that IS holding a license reaches the same trial
854
+ * (a token with no expiry recorded) and is told something different.
855
+ */
856
+ verification: "none",
857
+ token: null,
858
+ licenseKey: null,
859
+ expiresAt: null,
860
+ graceEndsAt: inDays(10),
861
+ }),
862
+ );
863
+
864
+ await openDialog();
865
+
866
+ expect(
867
+ await screen.findByText(
868
+ "Ask a master admin of this installation to add a license.",
869
+ ),
870
+ ).toBeInTheDocument();
871
+ expect(screen.queryByText(/below/)).not.toBeInTheDocument();
872
+ });
873
+
874
+ it("asks for a renewal rather than a refresh it cannot offer", async () => {
875
+ respondWith(
876
+ adminPayload({
877
+ status: "grace",
878
+ graceReason: "expired",
879
+ graceEndsAt: inDays(5),
880
+ expiresAt: inDays(-25),
881
+ }),
882
+ );
883
+
884
+ await openDialog();
885
+
886
+ expect(
887
+ await screen.findByText(
888
+ "Ask a master admin of this installation to renew the license.",
889
+ ),
890
+ ).toBeInTheDocument();
891
+ expect(screen.queryByText(/refresh it here/)).not.toBeInTheDocument();
892
+ });
893
+
894
+ it("explains who can activate a missing license", async () => {
895
+ respondWith(adminPayload({ status: "missing", licenseValid: false }));
896
+
897
+ await openDialog();
898
+
899
+ expect(
900
+ await screen.findByText("A master admin has to activate this license"),
901
+ ).toBeInTheDocument();
902
+ expect(
903
+ screen.getByText(
904
+ /A master admin can add the license to keep single sign-on, SCIM provisioning and audit logging running and enterprise configuration editable/,
905
+ ),
906
+ ).toBeInTheDocument();
907
+ });
908
+ });
909
+
910
+ describe("EditionLabel - the Community dialog", () => {
911
+ beforeEach(() => {
912
+ isEnterpriseEdition = false;
913
+ respondWith(
914
+ adminPayload({
915
+ edition: "community",
916
+ status: null,
917
+ verification: null,
918
+ licenseValid: false,
919
+ licenseKey: null,
920
+ token: null,
921
+ }),
922
+ );
923
+ });
924
+
925
+ it("has no license input, refresh or offline controls, and makes no POST", async () => {
926
+ const originalOpen: typeof window.open = window.open;
927
+ const opened: Array<string> = [];
928
+
929
+ window.open = ((url?: string | URL): Window | null => {
930
+ opened.push(String(url));
931
+ return null;
932
+ }) as typeof window.open;
933
+
934
+ try {
935
+ render(<EditionLabel />);
936
+ fireEvent.click(
937
+ screen.getByRole("button", { name: "Community Edition, Learn more" }),
938
+ );
939
+
940
+ expect(
941
+ await screen.findByText(/You are running the Community Edition/),
942
+ ).toBeInTheDocument();
943
+ await screen.findByText("This installation");
944
+
945
+ expectNoLicenseControls();
946
+
947
+ // The one primary action is the sales conversation, off-site.
948
+ fireEvent.click(screen.getByRole("button", { name: "Talk to Sales" }));
949
+
950
+ expect(opened).toEqual(["https://oneuptime.com/enterprise/demo"]);
951
+ expect(fetchCalls.length).toBeGreaterThan(0);
952
+ expect(postCalls()).toEqual([]);
953
+ } finally {
954
+ window.open = originalOpen;
955
+ }
956
+ });
957
+ });
958
+
959
+ /*
960
+ * The contract with a license manager, proved with a stand-in: what
961
+ * EditionLabel hands it, where the parts it returns land, and when it is
962
+ * rendered at all. The real manager (ee/AdminDashboard/License) is tested in
963
+ * ee/Tests/UI/License.
964
+ */
965
+ describe("EditionLabel - with a license manager", () => {
966
+ const managerProps: Array<LicenseManagerProps> = [];
967
+ let parts: LicenseManagerDialogParts = {};
968
+ let submitted: number = 0;
969
+
970
+ const FakeLicenseManager: (
971
+ props: LicenseManagerProps,
972
+ ) => React.ReactElement = (
973
+ props: LicenseManagerProps,
974
+ ): React.ReactElement => {
975
+ managerProps.push(props);
976
+
977
+ return props.renderDialog(parts);
978
+ };
979
+
980
+ const latestProps: () => LicenseManagerProps = (): LicenseManagerProps => {
981
+ const props: LicenseManagerProps | undefined =
982
+ managerProps[managerProps.length - 1];
983
+
984
+ expect(props).toBeDefined();
985
+
986
+ return props as LicenseManagerProps;
987
+ };
988
+
989
+ const renderWithManager: () => Promise<void> = async (): Promise<void> => {
990
+ render(<EditionLabel licenseManager={FakeLicenseManager} />);
991
+
992
+ await waitFor(() => {
993
+ expect(fetchCalls.length).toBeGreaterThan(0);
994
+ });
995
+ };
996
+
997
+ // Opens the dialog and waits for the license GET to fill its body in.
998
+ const openWithManager: () => Promise<void> = async (): Promise<void> => {
999
+ await renderWithManager();
1000
+ fireEvent.click(pill());
1001
+ await screen.findByText("This installation");
1002
+ };
1003
+
1004
+ const isBefore: (first: HTMLElement, second: HTMLElement) => boolean = (
1005
+ first: HTMLElement,
1006
+ second: HTMLElement,
1007
+ ): boolean => {
1008
+ return Boolean(
1009
+ first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING,
1010
+ );
1011
+ };
1012
+
1013
+ beforeEach(() => {
1014
+ managerProps.length = 0;
1015
+ submitted = 0;
1016
+ parts = {
1017
+ messages: <div data-testid="fake-messages" />,
1018
+ usage: <div data-testid="fake-usage" />,
1019
+ activation: <div data-testid="fake-activation" />,
1020
+ };
1021
+ });
1022
+
1023
+ it("puts each part where the dialog has always had it", async () => {
1024
+ await openWithManager();
1025
+
1026
+ const messages: HTMLElement = await screen.findByTestId("fake-messages");
1027
+ const details: HTMLElement = screen.getByText("Licensed to");
1028
+ const version: HTMLElement = screen.getByText("This installation");
1029
+ const usage: HTMLElement = screen.getByTestId("fake-usage");
1030
+ const activation: HTMLElement = screen.getByTestId("fake-activation");
1031
+
1032
+ expect(isBefore(messages, details)).toBe(true);
1033
+ expect(isBefore(details, version)).toBe(true);
1034
+ expect(isBefore(version, usage)).toBe(true);
1035
+ expect(isBefore(usage, activation)).toBe(true);
1036
+ });
1037
+
1038
+ it("keeps the status notices between the messages and the activation input, and the feature list last", async () => {
1039
+ isMasterAdmin = false;
1040
+ respondWith(publicPayload({ status: "missing", licenseValid: false }));
1041
+
1042
+ await renderWithManager();
1043
+ fireEvent.click(pill());
1044
+ await screen.findByText("What your license unlocks");
1045
+
1046
+ const messages: HTMLElement = await screen.findByTestId("fake-messages");
1047
+ const required: HTMLElement = screen.getByTestId(
1048
+ "enterprise-license-required-notice",
1049
+ );
1050
+ const askAnAdmin: HTMLElement = screen.getByText(
1051
+ "A master admin has to activate this license",
1052
+ );
1053
+ const activation: HTMLElement = screen.getByTestId("fake-activation");
1054
+ const features: HTMLElement = screen.getByText("What your license unlocks");
1055
+
1056
+ expect(isBefore(messages, required)).toBe(true);
1057
+ expect(isBefore(required, askAnAdmin)).toBe(true);
1058
+ expect(isBefore(askAnAdmin, activation)).toBe(true);
1059
+ expect(isBefore(activation, features)).toBe(true);
1060
+ });
1061
+
1062
+ it("gives the manager the dialog's primary button and its footer", async () => {
1063
+ parts = {
1064
+ ...parts,
1065
+ submitButtonText: "Fake Submit",
1066
+ onSubmit: (): void => {
1067
+ submitted += 1;
1068
+ },
1069
+ leftFooterElement: <button type="button">Fake footer action</button>,
1070
+ };
1071
+
1072
+ await openWithManager();
1073
+
1074
+ fireEvent.click(await screen.findByRole("button", { name: "Fake Submit" }));
1075
+
1076
+ expect(submitted).toBe(1);
1077
+ expect(
1078
+ screen.getByRole("button", { name: "Fake footer action" }),
1079
+ ).toBeInTheDocument();
1080
+ });
1081
+
1082
+ it("disables the primary button while the manager says so", async () => {
1083
+ parts = {
1084
+ ...parts,
1085
+ submitButtonText: "Fake Submit",
1086
+ onSubmit: (): void => {
1087
+ submitted += 1;
1088
+ },
1089
+ disableSubmitButton: true,
1090
+ };
1091
+
1092
+ await openWithManager();
1093
+
1094
+ expect(
1095
+ await screen.findByRole("button", { name: "Fake Submit" }),
1096
+ ).toBeDisabled();
1097
+ });
1098
+
1099
+ it("hands the manager what the license GET said", async () => {
1100
+ const body: JSONObject = adminPayload();
1101
+ respondWith(body);
1102
+
1103
+ await openWithManager();
1104
+
1105
+ await screen.findByText("Licensed to");
1106
+
1107
+ const license: LicenseManagerProps["license"] = latestProps().license;
1108
+
1109
+ expect(license.payload).toEqual(body);
1110
+ expect(license).toEqual(
1111
+ expect.objectContaining({
1112
+ isLoading: false,
1113
+ loadError: "",
1114
+ canManageLicense: true,
1115
+ licenseValid: true,
1116
+ isUnlicensedTrial: false,
1117
+ showLicenseDetails: true,
1118
+ companyName: "Acme Inc",
1119
+ licenseKey: "acme-license-key",
1120
+ userLimit: 50,
1121
+ currentUserCount: 10,
1122
+ effectiveUserCount: 10,
1123
+ isSeatLimitEnforced: true,
1124
+ canAddMoreUsers: true,
1125
+ seatTone: "healthy",
1126
+ seatUsageDisplayPercent: 20,
1127
+ currentVersion: "13.0.0",
1128
+ latestVersion: "13.0.0",
1129
+ hasComparableVersion: true,
1130
+ }),
1131
+ );
1132
+ });
1133
+
1134
+ it("tells the manager that somebody who is not a master admin cannot manage the license", async () => {
1135
+ isMasterAdmin = false;
1136
+ respondWith(publicPayload());
1137
+
1138
+ await renderWithManager();
1139
+ fireEvent.click(pill());
1140
+
1141
+ await screen.findByText("Licensed to");
1142
+
1143
+ expect(latestProps().license.canManageLicense).toBe(false);
1144
+ });
1145
+
1146
+ it("tells the manager about the trial", async () => {
1147
+ respondWith(
1148
+ adminPayload({
1149
+ status: "grace",
1150
+ graceReason: "unlicensed",
1151
+ // No license at all: no token, and nothing to verify.
1152
+ verification: "none",
1153
+ token: null,
1154
+ licenseKey: null,
1155
+ expiresAt: null,
1156
+ graceEndsAt: inDays(10),
1157
+ }),
1158
+ );
1159
+
1160
+ await renderWithManager();
1161
+ fireEvent.click(pill());
1162
+
1163
+ await screen.findByTestId("enterprise-license-trial-notice");
1164
+
1165
+ expect(latestProps().license.isUnlicensedTrial).toBe(true);
1166
+ expect(latestProps().license.showLicenseDetails).toBe(false);
1167
+ // With a manager to add it with, a master admin is pointed at it.
1168
+ expect(
1169
+ screen.getByText(
1170
+ "Add a license below before the trial ends to keep single sign-on, SCIM and audit logging running and enterprise configuration editable.",
1171
+ ),
1172
+ ).toBeInTheDocument();
1173
+ });
1174
+
1175
+ it("hands over no payload, and the reason, when the license could not be read", async () => {
1176
+ respond = (): HTTPErrorResponse => {
1177
+ return new HTTPErrorResponse(
1178
+ 500,
1179
+ { message: "The license service is down." },
1180
+ {},
1181
+ );
1182
+ };
1183
+
1184
+ await renderWithManager();
1185
+ fireEvent.click(pill());
1186
+
1187
+ expect(
1188
+ await screen.findByText("Unable to load license details"),
1189
+ ).toBeInTheDocument();
1190
+ expect(latestProps().license.payload).toBeNull();
1191
+ expect(latestProps().license.loadError).toBe(
1192
+ "The license service is down.",
1193
+ );
1194
+ });
1195
+
1196
+ it("stays mounted while the dialog is closed, and is told when it opens and closes", async () => {
1197
+ await renderWithManager();
1198
+
1199
+ expect(latestProps().isDialogOpen).toBe(false);
1200
+ expect(screen.queryByTestId("fake-messages")).not.toBeInTheDocument();
1201
+
1202
+ fireEvent.click(pill());
1203
+
1204
+ expect(await screen.findByTestId("fake-messages")).toBeInTheDocument();
1205
+ expect(latestProps().isDialogOpen).toBe(true);
1206
+
1207
+ fireEvent.click(screen.getByTestId("modal-footer-close-button"));
1208
+
1209
+ expect(screen.queryByTestId("fake-messages")).not.toBeInTheDocument();
1210
+ expect(latestProps().isDialogOpen).toBe(false);
1211
+ });
1212
+
1213
+ it("re-reads the license when the manager asks, with a fresh payload", async () => {
1214
+ await openWithManager();
1215
+ await screen.findByText("Licensed to");
1216
+
1217
+ const before: number = fetchCalls.length;
1218
+ const payloadBefore: LicenseManagerProps["license"]["payload"] =
1219
+ latestProps().license.payload;
1220
+
1221
+ await act(async () => {
1222
+ await latestProps().reloadLicense();
1223
+ });
1224
+
1225
+ expect(fetchCalls.length).toBe(before + 1);
1226
+ expect(fetchCalls[fetchCalls.length - 1]?.method).toBe("GET");
1227
+ expect(latestProps().license.payload).not.toBe(payloadBefore);
1228
+ expect(latestProps().license.payload).toEqual(payloadBefore);
1229
+ });
1230
+
1231
+ it("moves the pill when the manager applies the seat fields of a license write", async () => {
1232
+ await renderWithManager();
1233
+
1234
+ await waitFor(() => {
1235
+ expect(pill()).toHaveAccessibleName("Enterprise Edition, View details");
1236
+ });
1237
+
1238
+ act(() => {
1239
+ latestProps().applySeatEnforcement({
1240
+ isSeatLimitEnforced: true,
1241
+ seatsInUse: 50,
1242
+ canAddMoreUsers: false,
1243
+ });
1244
+ });
1245
+
1246
+ expect(pill()).toHaveAccessibleName(
1247
+ "Enterprise Edition, User limit exceeded",
1248
+ );
1249
+ expect(latestProps().license.seatTone).toBe("breached");
1250
+ expect(latestProps().license.effectiveUserCount).toBe(50);
1251
+ });
1252
+
1253
+ it("is never rendered on the Community Edition", async () => {
1254
+ isEnterpriseEdition = false;
1255
+
1256
+ render(<EditionLabel licenseManager={FakeLicenseManager} />);
1257
+ fireEvent.click(
1258
+ screen.getByRole("button", { name: "Community Edition, Learn more" }),
1259
+ );
1260
+
1261
+ expect(
1262
+ await screen.findByText(/You are running the Community Edition/),
1263
+ ).toBeInTheDocument();
1264
+ expect(managerProps).toHaveLength(0);
1265
+ expect(screen.queryByTestId("fake-messages")).not.toBeInTheDocument();
1266
+ });
1267
+
1268
+ it("is never rendered with billing enabled", async () => {
1269
+ billingEnabled = true;
1270
+
1271
+ const { container } = render(
1272
+ <EditionLabel licenseManager={FakeLicenseManager} />,
1273
+ );
1274
+
1275
+ await act(async () => {
1276
+ await Promise.resolve();
1277
+ });
1278
+
1279
+ expect(container).toBeEmptyDOMElement();
1280
+ expect(managerProps).toHaveLength(0);
1281
+ expect(fetchCalls).toHaveLength(0);
1282
+ });
1283
+ });
1284
+
1285
+ /*
1286
+ * The lapse checks the notices above rely on (EditionLabelLapseCopy.ts), and
1287
+ * the sentences EditionLabel builds its notices from. Each check has to fail
1288
+ * on the copy that promised SSO, SCIM and audit logging never stop, and on
1289
+ * copy that drops any single consequence - otherwise a passing notice test
1290
+ * proves nothing.
1291
+ */
1292
+ describe("EditionLabel - the lapse copy and its checks", () => {
1293
+ it.each(RETIRED_NOTICE_EXAMPLES)(
1294
+ "rejects the retired soft-enforcement copy: %s",
1295
+ (retired: string) => {
1296
+ expect(lapseWarningProblems(retired)).toContain("retired: never stop");
1297
+ expect(lapsedStateProblems(retired)).toContain("retired: never stop");
1298
+ expect(lapseWarningProblems(retired)).toContain(
1299
+ "missing: single sign-on (SAML and OIDC) stops",
1300
+ );
1301
+ expect(lapsedStateProblems(retired)).toContain(
1302
+ "missing: Single sign-on (SAML and OIDC) and SCIM provisioning are off",
1303
+ );
1304
+ },
1305
+ );
1306
+
1307
+ it("accepts the sentences EditionLabel ships", () => {
1308
+ expect(lapseWarningProblems(TRIAL_ENFORCEMENT_SUMMARY)).toEqual([]);
1309
+ expect(lapseWarningProblems(GRACE_ENFORCEMENT_SUMMARY)).toEqual([]);
1310
+ expect(lapsedStateProblems(LICENSE_LAPSED_STATE)).toEqual([]);
1311
+ });
1312
+
1313
+ it.each(
1314
+ LAPSE_WARNING_PHRASES.map((phrase: string) => {
1315
+ return [phrase];
1316
+ }),
1317
+ )("reports a warning that leaves out: %s", (phrase: string) => {
1318
+ const withoutPhrase: string = LICENSE_LAPSE_CONSEQUENCES.replace(
1319
+ phrase,
1320
+ "",
1321
+ );
1322
+
1323
+ expect(withoutPhrase).not.toBe(LICENSE_LAPSE_CONSEQUENCES);
1324
+ expect(lapseWarningProblems(withoutPhrase)).toEqual([`missing: ${phrase}`]);
1325
+ });
1326
+
1327
+ it.each(
1328
+ LAPSED_STATE_PHRASES.map((phrase: string) => {
1329
+ return [phrase];
1330
+ }),
1331
+ )("reports a lapsed notice that leaves out: %s", (phrase: string) => {
1332
+ const withoutPhrase: string = LICENSE_LAPSED_STATE.replace(phrase, "");
1333
+
1334
+ expect(withoutPhrase).not.toBe(LICENSE_LAPSED_STATE);
1335
+ expect(lapsedStateProblems(withoutPhrase)).toEqual([`missing: ${phrase}`]);
1336
+ });
1337
+
1338
+ it("reports shipped copy that also promises SSO keeps running", () => {
1339
+ expect(
1340
+ lapseWarningProblems(
1341
+ `${TRIAL_ENFORCEMENT_SUMMARY} Everything you already configured keeps working.`,
1342
+ ),
1343
+ ).toEqual(["retired: already configured keeps working"]);
1344
+ expect(
1345
+ lapsedStateProblems(
1346
+ `${LICENSE_LAPSED_STATE} SSO, SCIM and audit logging never stop.`,
1347
+ ),
1348
+ ).toEqual(["retired: never stop"]);
1349
+ });
1350
+
1351
+ it("warns without calling the trial a grace period, or the grace period a trial", () => {
1352
+ expect(LICENSE_LAPSE_CONSEQUENCES).not.toMatch(/grace|trial/i);
1353
+ expect(LICENSE_LAPSED_STATE).not.toMatch(/grace|trial/i);
1354
+ expect(TRIAL_ENFORCEMENT_SUMMARY).not.toMatch(/grace/i);
1355
+ expect(GRACE_ENFORCEMENT_SUMMARY).not.toMatch(/trial/i);
1356
+ });
1357
+
1358
+ /*
1359
+ * The trial is 14 days and the grace period after a license expires is 30.
1360
+ * Each summary states its own length, derived from the constant the license
1361
+ * classifier uses, so the two can never drift apart.
1362
+ */
1363
+ it("states a 14-day trial and a 30-day grace period, from the constants", () => {
1364
+ expect(ENTERPRISE_LICENSE_TRIAL_PERIOD_IN_DAYS).toBe(14);
1365
+ expect(ENTERPRISE_LICENSE_GRACE_PERIOD_IN_DAYS).toBe(30);
1366
+ expect(TRIAL_ENFORCEMENT_SUMMARY).toContain(
1367
+ `Without a valid license (after the ${ENTERPRISE_LICENSE_TRIAL_PERIOD_IN_DAYS}-day trial),`,
1368
+ );
1369
+ expect(GRACE_ENFORCEMENT_SUMMARY).toContain(
1370
+ `Without a valid license (after the ${ENTERPRISE_LICENSE_GRACE_PERIOD_IN_DAYS}-day grace period),`,
1371
+ );
1372
+ expect(GRACE_ENFORCEMENT_SUMMARY).not.toContain("14-day");
1373
+ });
1374
+ });
1375
+
1376
+ /*
1377
+ * A license IS installed, and the expiry beside it never was.
1378
+ *
1379
+ * The license client stopped calling that "invalid" - it is the product's own
1380
+ * bookkeeping failing, not an entitlement ending - and falls back to the
1381
+ * unlicensed trial instead. Two things followed for this dialog, and both were
1382
+ * wrong until these cases existed:
1383
+ *
1384
+ * - the explanation ("no expiry is recorded, re-activate it or let the daily
1385
+ * sync fetch it") was rendered in exactly one place, inside the "invalid"
1386
+ * branch, which this state deliberately is not. It reached nobody.
1387
+ * - the copy it fell into instead is the trial notice, which opens "No
1388
+ * Enterprise license is installed." - said to a customer who is holding
1389
+ * one.
1390
+ *
1391
+ * The server sends the token to a master admin and the verification to
1392
+ * everybody, so both audiences can tell the two apart.
1393
+ */
1394
+ describe("EditionLabel - a license whose expiry was never recorded", () => {
1395
+ const NO_EXPIRY_MESSAGE: string =
1396
+ "A OneUptime Enterprise license is installed, but no expiry is recorded for it, so this installation cannot tell whether it is still current. " +
1397
+ "A master admin can re-activate the license from the edition label in the Admin Dashboard, or leave the daily license sync to fetch its expiry from oneuptime.com.";
1398
+
1399
+ // Inside the trial the classifier reports grace/unlicensed, with a token.
1400
+ const inTrial: PayloadFunction = (
1401
+ overrides?: Record<string, unknown>,
1402
+ ): JSONObject => {
1403
+ return adminPayload({
1404
+ status: "grace",
1405
+ graceReason: "unlicensed",
1406
+ verification: "unverified",
1407
+ graceEndsAt: inDays(10),
1408
+ licenseValid: true,
1409
+ expiresAt: null,
1410
+ message: NO_EXPIRY_MESSAGE,
1411
+ userLimit: null,
1412
+ isSeatLimitEnforced: false,
1413
+ seatsInUse: null,
1414
+ seatsRemaining: null,
1415
+ ...(overrides || {}),
1416
+ });
1417
+ };
1418
+
1419
+ it("does not tell a customer holding a license that none is installed", async () => {
1420
+ respondWith(inTrial());
1421
+
1422
+ await openDialog();
1423
+
1424
+ const notice: HTMLElement = await screen.findByTestId(
1425
+ "enterprise-license-trial-notice",
1426
+ );
1427
+
1428
+ expect(notice).not.toHaveTextContent("No Enterprise license is installed");
1429
+ expect(notice).toHaveTextContent(
1430
+ "A license is installed, but this installation cannot tell whether it is still current",
1431
+ );
1432
+ });
1433
+
1434
+ it("shows the explanation the license client sent, inside the trial", async () => {
1435
+ respondWith(inTrial());
1436
+
1437
+ await openDialog();
1438
+
1439
+ expect(
1440
+ await screen.findByTestId("enterprise-license-status-message"),
1441
+ ).toHaveTextContent("no expiry is recorded for it");
1442
+ expect(
1443
+ screen.getByTestId("enterprise-license-status-message"),
1444
+ ).toHaveTextContent("daily license sync");
1445
+ });
1446
+
1447
+ /*
1448
+ * And the remedy names the license that is there. This dialog is rendered
1449
+ * with no license manager, so nobody here can act on it and the copy is the
1450
+ * "ask a master admin" one - re-activate the license, not add a first.
1451
+ */
1452
+ it("asks for the installed license to be re-activated, not for a new one", async () => {
1453
+ respondWith(inTrial());
1454
+
1455
+ await openDialog();
1456
+
1457
+ const notice: HTMLElement = await screen.findByTestId(
1458
+ "enterprise-license-trial-notice",
1459
+ );
1460
+
1461
+ expect(notice).toHaveTextContent(
1462
+ "Ask a master admin of this installation to re-activate the license",
1463
+ );
1464
+ expect(notice).not.toHaveTextContent("to add a license");
1465
+ });
1466
+
1467
+ /*
1468
+ * Past the trial the classifier reports "missing", which used to render the
1469
+ * bare "This installation has no valid Enterprise license." and nothing
1470
+ * else - the one state where an administrator most needs to be told what is
1471
+ * actually wrong.
1472
+ */
1473
+ it("shows the explanation past the trial, where the status alone says nothing", async () => {
1474
+ respondWith(
1475
+ adminPayload({
1476
+ status: "missing",
1477
+ verification: "unverified",
1478
+ licenseValid: false,
1479
+ expiresAt: null,
1480
+ graceEndsAt: inDays(-1),
1481
+ message: NO_EXPIRY_MESSAGE,
1482
+ }),
1483
+ );
1484
+
1485
+ await openDialog();
1486
+
1487
+ const notice: HTMLElement = await screen.findByTestId(
1488
+ "enterprise-license-required-notice",
1489
+ );
1490
+
1491
+ expect(notice).toHaveTextContent("no expiry is recorded for it");
1492
+ expect(notice).toHaveTextContent("re-activate the license");
1493
+ expect(notice).not.toHaveTextContent(
1494
+ "This installation has no valid Enterprise license",
1495
+ );
1496
+ });
1497
+
1498
+ /*
1499
+ * Somebody who is not a master admin is sent no token and no message, but is
1500
+ * sent the verification - which is "none" only when there was no token to
1501
+ * judge. So they are not told that no license is installed either.
1502
+ */
1503
+ it("tells a non-master-admin the same thing, from the verification alone", async () => {
1504
+ isMasterAdmin = false;
1505
+ respondWith(
1506
+ publicPayload({
1507
+ status: "grace",
1508
+ graceReason: "unlicensed",
1509
+ verification: "unverified",
1510
+ graceEndsAt: inDays(10),
1511
+ expiresAt: null,
1512
+ }),
1513
+ );
1514
+
1515
+ await openDialog();
1516
+
1517
+ const notice: HTMLElement = await screen.findByTestId(
1518
+ "enterprise-license-trial-notice",
1519
+ );
1520
+
1521
+ expect(notice).not.toHaveTextContent("No Enterprise license is installed");
1522
+ expect(notice).toHaveTextContent(
1523
+ "Ask a master admin of this installation to re-activate the license",
1524
+ );
1525
+ // No message is sent to them, so none is rendered.
1526
+ expect(
1527
+ screen.queryByTestId("enterprise-license-status-message"),
1528
+ ).not.toBeInTheDocument();
1529
+ });
1530
+
1531
+ /*
1532
+ * The genuinely unlicensed installation, unchanged. The whole point of the
1533
+ * qualification is that it applies only when a license IS there: verification
1534
+ * "none" and no token.
1535
+ */
1536
+ it("leaves the copy for an install with no license at all exactly as it was", async () => {
1537
+ respondWith(
1538
+ adminPayload({
1539
+ status: "grace",
1540
+ graceReason: "unlicensed",
1541
+ verification: "none",
1542
+ graceEndsAt: inDays(10),
1543
+ licenseValid: true,
1544
+ token: null,
1545
+ licenseKey: null,
1546
+ expiresAt: null,
1547
+ companyName: null,
1548
+ }),
1549
+ );
1550
+
1551
+ await openDialog();
1552
+
1553
+ const notice: HTMLElement = await screen.findByTestId(
1554
+ "enterprise-license-trial-notice",
1555
+ );
1556
+
1557
+ expect(notice).toHaveTextContent("No Enterprise license is installed");
1558
+ expect(notice).toHaveTextContent(
1559
+ "Ask a master admin of this installation to add a license",
1560
+ );
1561
+ expect(notice).not.toHaveTextContent("A license is installed");
1562
+ });
1563
+
1564
+ /*
1565
+ * An "invalid" license still shows its message exactly once: it is both the
1566
+ * reason line and the message, and a master admin must not read the same
1567
+ * sentence twice.
1568
+ */
1569
+ it("does not repeat the message for an invalid license", async () => {
1570
+ respondWith(
1571
+ adminPayload({
1572
+ status: "invalid",
1573
+ licenseValid: false,
1574
+ message: "The license is bound to a different OneUptime instance.",
1575
+ }),
1576
+ );
1577
+
1578
+ await openDialog();
1579
+
1580
+ await screen.findByTestId("enterprise-license-required-notice");
1581
+
1582
+ expect(
1583
+ screen.getAllByText(/bound to a different OneUptime instance/),
1584
+ ).toHaveLength(1);
1585
+ });
1586
+ });