@oneuptime/common 13.0.8 → 14.0.0

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 (333) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Monitor.ts +3 -2
  4. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  5. package/Scripts/generate-service-worker.js +65 -4
  6. package/Server/API/BaseAPI.ts +26 -5
  7. package/Server/API/GlobalConfigAPI.ts +228 -462
  8. package/Server/API/ProjectOIDC.ts +25 -16
  9. package/Server/API/ProjectSSO.ts +24 -16
  10. package/Server/API/StatusPageAPI.ts +100 -42
  11. package/Server/API/TeamMemberAPI.ts +10 -1
  12. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  13. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  14. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  15. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  16. package/Server/EnvironmentConfig.ts +147 -2
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  19. package/Server/Middleware/UserAuthorization.ts +38 -16
  20. package/Server/Services/AuditLogService.ts +84 -1079
  21. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  22. package/Server/Services/GlobalConfigService.ts +80 -1
  23. package/Server/Services/MonitorProbeService.ts +116 -20
  24. package/Server/Services/MonitorService.ts +56 -4
  25. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  26. package/Server/Services/MonitorTemplateService.ts +26 -0
  27. package/Server/Services/ProjectService.ts +284 -0
  28. package/Server/Services/RumSessionPinService.ts +25 -6
  29. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  30. package/Server/Services/StatusPageService.ts +78 -1
  31. package/Server/Services/TeamMemberService.ts +11 -0
  32. package/Server/Services/TeamService.ts +10 -0
  33. package/Server/Services/UserService.ts +10 -17
  34. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  35. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  36. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  37. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  38. package/Server/Types/Markdown.ts +41 -0
  39. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  40. package/Server/Utils/EditionEnforcement.ts +233 -0
  41. package/Server/Utils/FrontendEnvironment.ts +41 -2
  42. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  43. package/Server/Utils/Monitor/AffectedResourceList.ts +190 -0
  44. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  45. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +543 -333
  46. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  47. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  48. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  49. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  50. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  51. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  52. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  53. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  54. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  55. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  56. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  57. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  58. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  59. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  60. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  61. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  62. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  63. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  64. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  65. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  66. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  67. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  68. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  69. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  70. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  71. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  72. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  73. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  74. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  75. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  76. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  77. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  78. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  79. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  80. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  81. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  82. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  83. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  84. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  85. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  86. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  87. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  88. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  89. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  90. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  91. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  92. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  93. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  95. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  96. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  97. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  98. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  99. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  100. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  101. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  102. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  103. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  104. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  105. package/Tests/Server/Types/Markdown.test.ts +181 -0
  106. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  107. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  108. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  109. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  110. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  111. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  112. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +934 -96
  113. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  114. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  115. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  116. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  117. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  118. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  119. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  120. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  121. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  122. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  123. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  124. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  125. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  126. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  127. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  128. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  129. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  130. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  131. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  132. package/Types/API/Hostname.ts +63 -16
  133. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  134. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  135. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  136. package/Types/Database/CanonicalModelInstance.ts +102 -0
  137. package/Types/Database/TableColumn.ts +20 -6
  138. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  139. package/Types/IP/IPv6.ts +6 -1
  140. package/Types/Monitor/MonitorStep.ts +6 -1
  141. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  142. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  143. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  144. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  145. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  146. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  147. package/UI/Components/ModelList/ModelList.tsx +11 -1
  148. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  149. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  150. package/UI/esbuild-enterprise.js +263 -0
  151. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  152. package/Utils/HostAddressUtil.ts +94 -0
  153. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  154. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  155. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  156. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  157. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  159. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  161. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  163. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  164. package/build/dist/Server/API/BaseAPI.js +26 -5
  165. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  166. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  167. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  168. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  169. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  170. package/build/dist/Server/API/ProjectSSO.js +24 -16
  171. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  172. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  173. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  174. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  175. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  176. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  177. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  178. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  179. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  180. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  181. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  182. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  183. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  184. package/build/dist/Server/EnvironmentConfig.js +84 -2
  185. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  190. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  191. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  192. package/build/dist/Server/Services/AuditLogService.js +71 -740
  193. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  194. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  195. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  196. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  197. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  198. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  199. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  200. package/build/dist/Server/Services/MonitorService.js +45 -4
  201. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  202. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  203. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  204. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  205. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  206. package/build/dist/Server/Services/ProjectService.js +214 -0
  207. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  208. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  209. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  210. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  211. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  212. package/build/dist/Server/Services/StatusPageService.js +71 -0
  213. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  214. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  215. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  216. package/build/dist/Server/Services/TeamService.js +9 -0
  217. package/build/dist/Server/Services/TeamService.js.map +1 -1
  218. package/build/dist/Server/Services/UserService.js +10 -16
  219. package/build/dist/Server/Services/UserService.js.map +1 -1
  220. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  221. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  222. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  223. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  224. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  225. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  226. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  227. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  228. package/build/dist/Server/Types/Markdown.js +31 -0
  229. package/build/dist/Server/Types/Markdown.js.map +1 -1
  230. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  231. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  232. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  233. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  234. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  235. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  236. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  237. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  238. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +96 -0
  239. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  240. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  241. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  242. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +449 -261
  243. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  244. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  245. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  246. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  247. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  248. package/build/dist/Types/API/Hostname.js +59 -11
  249. package/build/dist/Types/API/Hostname.js.map +1 -1
  250. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  251. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  252. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  253. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  254. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  255. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  256. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  257. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  258. package/build/dist/Types/Database/TableColumn.js +19 -6
  259. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  260. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  261. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  262. package/build/dist/Types/IP/IPv6.js +6 -1
  263. package/build/dist/Types/IP/IPv6.js.map +1 -1
  264. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  265. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  266. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  267. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  268. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  269. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  270. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  271. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  272. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  273. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  274. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  275. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  276. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  277. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  278. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  279. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  280. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  281. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  282. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  283. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  284. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  285. package/build/dist/Utils/HostAddressUtil.js +80 -0
  286. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  287. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  288. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  289. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  290. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  291. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  292. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  293. package/jest.config.json +3 -1
  294. package/package.json +1 -1
  295. package/test-setup.sh +103 -34
  296. package/tsconfig.json +13 -0
  297. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  298. package/Server/API/TeamComplianceAPI.ts +0 -158
  299. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  300. package/Server/Services/TeamComplianceService.ts +0 -733
  301. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  302. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  303. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  304. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  305. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  306. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  307. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  308. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  309. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  310. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  311. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  312. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  313. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  314. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  315. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  316. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  317. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  318. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  319. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  320. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  321. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  322. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  323. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  324. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  325. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  326. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  327. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  328. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  329. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  330. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  331. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  332. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  333. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -1,7 +1,5 @@
1
1
  import { mockRouter } from "./Helpers";
2
2
  import OnCallReadinessAPI from "../../../Server/API/OnCallReadinessAPI";
3
- import TeamComplianceAPI from "../../../Server/API/TeamComplianceAPI";
4
- import TeamComplianceService from "../../../Server/Services/TeamComplianceService";
5
3
  import CommonAPI from "../../../Server/API/CommonAPI";
6
4
  import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
7
5
  import OnCallReadinessService, {
@@ -21,9 +19,7 @@ import OnCallDutyPolicyScheduleLayerUserService from "../../../Server/Services/O
21
19
  import OnCallDutyPolicyService from "../../../Server/Services/OnCallDutyPolicyService";
22
20
  import OnCallDutyPolicyUserOverrideService from "../../../Server/Services/OnCallDutyPolicyUserOverrideService";
23
21
  import ProjectService from "../../../Server/Services/ProjectService";
24
- import TeamComplianceSettingService from "../../../Server/Services/TeamComplianceSettingService";
25
22
  import TeamMemberService from "../../../Server/Services/TeamMemberService";
26
- import TeamService from "../../../Server/Services/TeamService";
27
23
  import UserCallService from "../../../Server/Services/UserCallService";
28
24
  import UserEmailService from "../../../Server/Services/UserEmailService";
29
25
  import UserMicrosoftTeamsService from "../../../Server/Services/UserMicrosoftTeamsService";
@@ -51,7 +47,6 @@ import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
51
47
  import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
52
48
  import UserWhatsApp from "../../../Models/DatabaseModels/UserWhatsApp";
53
49
  import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
54
- import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
55
50
  import Dictionary from "../../../Types/Dictionary";
56
51
  import Email from "../../../Types/Email";
57
52
  import BadDataException from "../../../Types/Exception/BadDataException";
@@ -66,20 +61,16 @@ import Permission, {
66
61
  UserTenantAccessPermission,
67
62
  } from "../../../Types/Permission";
68
63
  import Phone from "../../../Types/Phone";
69
- import ComplianceRuleType from "../../../Types/Team/ComplianceRuleType";
70
64
  import UserType from "../../../Types/UserType";
71
- import {
72
- afterEach,
73
- beforeAll,
74
- beforeEach,
75
- describe,
76
- expect,
77
- test,
78
- } from "@jest/globals";
65
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
79
66
 
80
67
  /*
81
- * The HTTP face of Phase 2, plus the TeamComplianceService rebuild that now sits
82
- * behind it.
68
+ * The HTTP face of Phase 2.
69
+ *
70
+ * The team compliance route (/team/compliance-status/:teamId) and the
71
+ * TeamComplianceService rebuild behind it are Enterprise Edition code since the
72
+ * Community / Enterprise split; their HTTP tests moved with them to
73
+ * ee/Tests/Server/TeamCompliance/TeamComplianceAPI.test.ts.
83
74
  *
84
75
  * Two different levels of test live in this file on purpose, because the two
85
76
  * risks are different:
@@ -92,16 +83,12 @@ import {
92
83
  * answered identically so the endpoints cannot be used to enumerate ids
93
84
  * across tenants.
94
85
  *
95
- * MASKING AND THE COMPLIANCE REBUILD are tested end to end, with the REAL
96
- * OnCallReadinessService and only the database-facing services stubbed. A
97
- * masking test that stubs the service it is testing proves nothing: the whole
98
- * claim is that a raw phone number put into the database cannot come out of
99
- * the HTTP response, and the only way to test that claim is to put a raw phone
100
- * number in one end and read the JSON out of the other. The same reasoning
101
- * applies to the compliance rebuild - TeamComplianceServiceBehaviour.test.ts
102
- * already pins its mapping with readiness stubbed, so what is left worth
103
- * proving is that the real seam between the two behaves, which is exactly
104
- * where the four defects used to live.
86
+ * MASKING is tested end to end, with the REAL OnCallReadinessService and only
87
+ * the database-facing services stubbed. A masking test that stubs the service
88
+ * it is testing proves nothing: the whole claim is that a raw phone number put
89
+ * into the database cannot come out of the HTTP response, and the only way to
90
+ * test that claim is to put a raw phone number in one end and read the JSON
91
+ * out of the other.
105
92
  *
106
93
  * These routes are mounted with UserMiddleware.getUserMiddleware, which admits
107
94
  * anonymous callers as UserType.Public and takes the project from a
@@ -151,7 +138,6 @@ const POLICY_ROUTE: string = "/on-call-readiness/policy/:policyId";
151
138
  const PROJECT_ROUTE: string = "/on-call-readiness/project";
152
139
  const USER_ROUTE: string = "/on-call-readiness/user/:userId";
153
140
  const SETUP_REMINDER_ROUTE: string = "/on-call-readiness/send-setup-reminder";
154
- const COMPLIANCE_ROUTE: string = "/team/compliance-status/:teamId";
155
141
 
156
142
  /*
157
143
  * The message every authorisation refusal on these routes carries. Asserting on
@@ -208,19 +194,6 @@ interface RouteCallResult {
208
194
  nextCallCount: number;
209
195
  }
210
196
 
211
- /** A minimal user row, as UserService.findBy would hand one back. */
212
- interface StubUser {
213
- id: ObjectID;
214
- name: string;
215
- email: string;
216
- }
217
-
218
- /** A minimal severity row - the shape both severity services return here. */
219
- interface StubSeverity {
220
- id: ObjectID;
221
- name: string;
222
- }
223
-
224
197
  function buildMemberProps(data: {
225
198
  projectId: ObjectID;
226
199
  userId: ObjectID;
@@ -335,7 +308,7 @@ function firstCall(spy: jest.SpyInstance): CapturedFindBy {
335
308
 
336
309
  let propsSpy: jest.SpyInstance;
337
310
 
338
- // Every database-facing read the readiness / compliance path can make.
311
+ // Every database-facing read the readiness path can make.
339
312
  let policyFindOneById: jest.SpyInstance;
340
313
  let escalationUserFindBy: jest.SpyInstance;
341
314
  let escalationTeamFindBy: jest.SpyInstance;
@@ -357,9 +330,6 @@ let notificationRuleFindBy: jest.SpyInstance;
357
330
  let incidentSeverityFindBy: jest.SpyInstance;
358
331
  let alertSeverityFindBy: jest.SpyInstance;
359
332
  let projectFindOneById: jest.SpyInstance;
360
- let teamFindOneById: jest.SpyInstance;
361
- let teamFindOneBy: jest.SpyInstance;
362
- let complianceSettingFindBy: jest.SpyInstance;
363
333
 
364
334
  function everyFindBySpy(): Array<jest.SpyInstance> {
365
335
  return [
@@ -382,7 +352,6 @@ function everyFindBySpy(): Array<jest.SpyInstance> {
382
352
  notificationRuleFindBy,
383
353
  incidentSeverityFindBy,
384
354
  alertSeverityFindBy,
385
- complianceSettingFindBy,
386
355
  ];
387
356
  }
388
357
 
@@ -391,20 +360,6 @@ let otherProjectId: ObjectID;
391
360
  let callerUserId: ObjectID;
392
361
  let policyId: ObjectID;
393
362
  let subjectUserId: ObjectID;
394
- let teamId: ObjectID;
395
-
396
- beforeAll(() => {
397
- /*
398
- * OnCallReadinessAPI registers its routes when the MODULE is evaluated - it
399
- * exports a bare router rather than a class - so its three routes are already
400
- * on mockRouter by the time this runs. TeamComplianceAPI registers in its
401
- * constructor, so it needs the explicit `new`. Nothing clears
402
- * mockRouter.routes here for exactly that reason: doing so would throw away
403
- * the readiness routes, which cannot be re-registered without re-importing
404
- * the module.
405
- */
406
- new TeamComplianceAPI();
407
- });
408
363
 
409
364
  beforeEach(() => {
410
365
  jest.clearAllMocks();
@@ -423,7 +378,6 @@ beforeEach(() => {
423
378
  callerUserId = ObjectID.generate();
424
379
  policyId = ObjectID.generate();
425
380
  subjectUserId = ObjectID.generate();
426
- teamId = ObjectID.generate();
427
381
  stubEmailMethodId = ObjectID.generate();
428
382
  stubTeamId = ObjectID.generate();
429
383
 
@@ -511,30 +465,6 @@ beforeEach(() => {
511
465
  enableWhatsAppNotifications: true,
512
466
  enableTelegramNotifications: true,
513
467
  } as never);
514
-
515
- /*
516
- * The team is read TWICE on the compliance route, by two different methods,
517
- * and both are stubbed because they answer two different questions.
518
- *
519
- * findOneById is the route's authorisation read: it fetches the team's own
520
- * projectId so the handler can refuse a team that belongs to somebody else.
521
- * The stub therefore carries a projectId - a row without one is refused, which
522
- * is the behaviour the "foreign team" tests below rely on.
523
- *
524
- * findOneBy is the service's own read, scoped to id AND project in the query,
525
- * so that an in-process caller reaching the service directly cannot resolve a
526
- * foreign team either.
527
- */
528
- teamFindOneById = jest.spyOn(TeamService, "findOneById").mockResolvedValue({
529
- _id: teamId.toString(),
530
- projectId: projectId,
531
- } as never);
532
- teamFindOneBy = jest
533
- .spyOn(TeamService, "findOneBy")
534
- .mockResolvedValue({ name: "Platform On-Call" } as never);
535
- complianceSettingFindBy = jest
536
- .spyOn(TeamComplianceSettingService, "findBy")
537
- .mockResolvedValue([] as never);
538
468
  });
539
469
 
540
470
  afterEach(() => {
@@ -2121,837 +2051,33 @@ describe("GET /on-call-readiness/user/:userId - identifier masking", () => {
2121
2051
 
2122
2052
  /*
2123
2053
  * --------------------------------------------------------------------------- *
2124
- * The TeamComplianceService rebuild, exercised through its own route with the
2125
- * REAL readiness service underneath.
2126
- *
2127
- * Teams > View > Compliance renders this payload field for field - including
2128
- * the reason strings, which it prints as prose rather than mapping through any
2129
- * lookup - so the shape is a hard contract. What changed underneath is where
2130
- * the two "does this user have on-call rules?" answers come from, and the four
2131
- * defects that lived in the old answer are re-tested here across the real seam
2132
- * rather than against a stubbed readiness service.
2054
+ * A refused caller costs nothing. Every database-facing read the readiness
2055
+ * routes can reach is stubbed above; an anonymous caller must be turned away
2056
+ * before ANY of them runs, on every read route - the refusal is the only gate,
2057
+ * so it has to come first.
2133
2058
  * ---------------------------------------------------------------------------
2134
2059
  */
2135
2060
 
2136
- interface StubRule {
2137
- _id: string;
2138
- userId: ObjectID;
2139
- ruleType: NotificationRuleType;
2140
- incidentSeverityId?: ObjectID | undefined;
2141
- alertSeverityId?: ObjectID | undefined;
2142
- isOptOut?: boolean | undefined;
2143
- userCallId?: ObjectID | undefined;
2144
- userSmsId?: ObjectID | undefined;
2145
- userEmailId?: ObjectID | undefined;
2146
- userPushId?: ObjectID | undefined;
2147
- userTelegramId?: ObjectID | undefined;
2148
- userSlackId?: ObjectID | undefined;
2149
- userMicrosoftTeamsId?: ObjectID | undefined;
2150
- userWhatsAppId?: ObjectID | undefined;
2151
- userWebhookId?: ObjectID | undefined;
2152
- }
2153
-
2154
- describe("GET /team/compliance-status/:teamId - the rebuilt service", () => {
2155
- let ada: StubUser;
2156
- let grace: StubUser;
2157
- let critical: StubSeverity;
2158
- let major: StubSeverity;
2159
- let page: StubSeverity;
2160
-
2161
- function stage(data: {
2162
- settings: Array<{ ruleType: ComplianceRuleType; enabled: boolean }>;
2163
- members: Array<StubUser>;
2164
- rules: Array<StubRule>;
2165
- incidentSeverities: Array<StubSeverity>;
2166
- alertSeverities: Array<StubSeverity>;
2167
- }): void {
2168
- complianceSettingFindBy.mockResolvedValue(data.settings as never);
2169
-
2170
- teamMemberFindBy.mockResolvedValue(
2171
- data.members.map((member: StubUser): Record<string, unknown> => {
2172
- return { _id: `tm-${member.id.toString()}`, userId: member.id };
2173
- }) as never,
2174
- );
2175
-
2176
- userFindBy.mockResolvedValue(data.members as never);
2177
-
2178
- /*
2179
- * The members are attached DIRECTLY to an escalation rule, which is the
2180
- * arrangement that lets the project-scope readiness summary answer for all
2181
- * of them in one pass. It is also the arrangement TeamComplianceService is
2182
- * designed around: a team on a compliance page exists because its members
2183
- * are on call.
2184
- */
2185
- escalationUserFindBy.mockResolvedValue(
2186
- data.members.map((member: StubUser): Record<string, unknown> => {
2187
- return { _id: `er-${member.id.toString()}`, userId: member.id };
2188
- }) as never,
2189
- );
2190
-
2191
- notificationRuleFindBy.mockResolvedValue(data.rules as never);
2192
- incidentSeverityFindBy.mockResolvedValue(data.incidentSeverities as never);
2193
- alertSeverityFindBy.mockResolvedValue(data.alertSeverities as never);
2194
- }
2195
-
2196
- async function readCompliance(): Promise<Record<string, unknown>> {
2197
- const result: RouteCallResult = await callGetRoute({
2198
- uri: COMPLIANCE_ROUTE,
2199
- params: { teamId: teamId.toString() },
2200
- });
2201
-
2202
- expect(result.nextCallCount).toBe(0);
2203
-
2204
- return jsonPayload();
2205
- }
2206
-
2207
- function statusesOf(
2208
- payload: Record<string, unknown>,
2209
- ): Array<Record<string, unknown>> {
2210
- return payload["userComplianceStatuses"] as Array<Record<string, unknown>>;
2211
- }
2212
-
2213
- beforeEach(() => {
2214
- ada = { id: ObjectID.generate(), name: "Ada", email: "ada@example.com" };
2215
- grace = {
2216
- id: ObjectID.generate(),
2217
- name: "Grace",
2218
- email: "grace@example.com",
2219
- };
2220
- critical = { id: ObjectID.generate(), name: "Critical" };
2221
- major = { id: ObjectID.generate(), name: "Major" };
2222
- page = { id: ObjectID.generate(), name: "Page" };
2223
- });
2224
-
2225
- test("the response shape the dashboard renders is unchanged", async () => {
2226
- stage({
2227
- settings: [
2228
- {
2229
- ruleType: ComplianceRuleType.HasNotificationEmailMethod,
2230
- enabled: true,
2231
- },
2232
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2233
- ],
2234
- members: [ada],
2235
- rules: [],
2236
- incidentSeverities: [critical],
2237
- alertSeverities: [],
2238
- });
2239
-
2240
- const payload: Record<string, unknown> = await readCompliance();
2241
-
2242
- expect(Object.keys(payload).sort()).toEqual([
2243
- "complianceSettings",
2244
- "teamId",
2245
- "teamName",
2246
- "userComplianceStatuses",
2247
- ]);
2248
- expect(payload["teamId"]).toBe(teamId.toString());
2249
- expect(payload["teamName"]).toBe("Platform On-Call");
2250
- expect(payload["complianceSettings"]).toEqual([
2251
- {
2252
- ruleType: ComplianceRuleType.HasNotificationEmailMethod,
2253
- enabled: true,
2254
- },
2255
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2256
- ]);
2257
-
2258
- const statuses: Array<Record<string, unknown>> = statusesOf(payload);
2259
- expect(statuses).toHaveLength(1);
2260
- expect(Object.keys(statuses[0]!).sort()).toEqual([
2261
- "isCompliant",
2262
- "nonCompliantRules",
2263
- "userEmail",
2264
- "userId",
2265
- "userName",
2266
- "userProfilePictureId",
2267
- ]);
2268
- expect(statuses[0]!["userId"]).toBe(ada.id.toString());
2269
- expect(statuses[0]!["userName"]).toBe("Ada");
2270
- expect(statuses[0]!["isCompliant"]).toBe(false);
2271
- /*
2272
- * The reason strings are printed verbatim by TeamComplianceStatusTable, so
2273
- * their exact wording is part of the payload contract, not an internal
2274
- * detail. Both an old-style channel rule and a rebuilt on-call rule are
2275
- * asserted together to show neither vocabulary shifted.
2276
- */
2277
- expect(statuses[0]!["nonCompliantRules"]).toEqual([
2278
- {
2279
- ruleType: ComplianceRuleType.HasNotificationEmailMethod,
2280
- reason: "No verified email address configured for notifications",
2281
- },
2282
- {
2283
- ruleType: ComplianceRuleType.HasIncidentOnCallRules,
2284
- reason: "Missing notification rules for incident severities: Critical",
2285
- },
2286
- ]);
2287
- });
2288
-
2289
- test("DEFECT closed: a rule on Telegram, WhatsApp, Slack, Teams or a webhook now counts", async () => {
2290
- /*
2291
- * The old check read userCallId/userSmsId/userEmailId/userPushId off the
2292
- * rule row and treated a row carrying none of them as no rule at all, so a
2293
- * responder reachable only on Telegram, WhatsApp or a webhook was reported
2294
- * non-compliant while the runtime was quite happily paging them. A false RED
2295
- * teaches admins to ignore the table, which is worse than no table. Slack
2296
- * and Microsoft Teams arrived after the fix and are staged alongside so the
2297
- * defect cannot return for the channels that never lived through it.
2298
- *
2299
- * All five channels are staged onto one user at once: whichever column the
2300
- * rule carries, the row is a rule.
2301
- */
2302
- const minor: StubSeverity = { id: ObjectID.generate(), name: "Minor" };
2303
- const warn: StubSeverity = { id: ObjectID.generate(), name: "Warn" };
2304
-
2305
- stage({
2306
- settings: [
2307
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2308
- { ruleType: ComplianceRuleType.HasAlertOnCallRules, enabled: true },
2309
- ],
2310
- members: [ada],
2311
- rules: [
2312
- {
2313
- _id: "rule-telegram",
2314
- userId: ada.id,
2315
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2316
- incidentSeverityId: critical.id,
2317
- userTelegramId: ObjectID.generate(),
2318
- },
2319
- {
2320
- _id: "rule-whatsapp",
2321
- userId: ada.id,
2322
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2323
- incidentSeverityId: major.id,
2324
- userWhatsAppId: ObjectID.generate(),
2325
- },
2326
- {
2327
- _id: "rule-slack",
2328
- userId: ada.id,
2329
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2330
- incidentSeverityId: minor.id,
2331
- userSlackId: ObjectID.generate(),
2332
- },
2333
- {
2334
- _id: "rule-webhook",
2335
- userId: ada.id,
2336
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
2337
- alertSeverityId: page.id,
2338
- userWebhookId: ObjectID.generate(),
2339
- },
2340
- {
2341
- _id: "rule-microsoft-teams",
2342
- userId: ada.id,
2343
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
2344
- alertSeverityId: warn.id,
2345
- userMicrosoftTeamsId: ObjectID.generate(),
2346
- },
2347
- ],
2348
- incidentSeverities: [critical, major, minor],
2349
- alertSeverities: [page, warn],
2350
- });
2351
-
2352
- const statuses: Array<Record<string, unknown>> = statusesOf(
2353
- await readCompliance(),
2354
- );
2355
-
2356
- expect(statuses[0]!["isCompliant"]).toBe(true);
2357
- expect(statuses[0]!["nonCompliantRules"]).toEqual([]);
2358
- });
2359
-
2360
- test("DEFECT closed: no channel column is even read, so none can be missed", async () => {
2361
- /*
2362
- * The structural half of the fix. The three formerly-invisible channels were
2363
- * invisible because they were never SELECTed; asserting that NONE of the
2364
- * nine is selected means no future edit can reintroduce a partial column
2365
- * list and quietly start under-counting again.
2366
- */
2367
- stage({
2368
- settings: [
2369
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2370
- ],
2371
- members: [ada],
2372
- rules: [],
2373
- incidentSeverities: [critical],
2374
- alertSeverities: [],
2375
- });
2376
-
2377
- await readCompliance();
2378
-
2379
- const call: CapturedFindBy = firstCall(notificationRuleFindBy);
2380
-
2381
- for (const column of [
2382
- "userCallId",
2383
- "userSmsId",
2384
- "userEmailId",
2385
- "userPushId",
2386
- "userTelegramId",
2387
- "userSlackId",
2388
- "userMicrosoftTeamsId",
2389
- "userWhatsAppId",
2390
- "userWebhookId",
2391
- ]) {
2392
- expect(call.select?.[column]).toBeUndefined();
2393
- }
2394
-
2395
- // What IS read: who, what kind of page, which severity, and opt-out state.
2396
- expect(call.select?.["ruleType"]).toBe(true);
2397
- expect(call.select?.["incidentSeverityId"]).toBe(true);
2398
- expect(call.select?.["alertSeverityId"]).toBe(true);
2399
- expect(call.select?.["isOptOut"]).toBe(true);
2400
- });
2401
-
2402
- test("DEFECT closed: a WHEN_USER_GOES_OFF_CALL rule is no longer incident coverage", async () => {
2403
- /*
2404
- * The false GREEN, and the more dangerous of the two directions: the owner
2405
- * was told a responder was covered for Sev1 incidents when their only rule
2406
- * fired as they went off call. The row below carries an incidentSeverityId,
2407
- * which is exactly what made the old severity-only match accept it.
2408
- */
2409
- stage({
2410
- settings: [
2411
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2412
- ],
2413
- members: [ada],
2414
- rules: [
2415
- {
2416
- _id: "rule-off-call",
2417
- userId: ada.id,
2418
- ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
2419
- incidentSeverityId: critical.id,
2420
- userEmailId: ObjectID.generate(),
2421
- },
2422
- ],
2423
- incidentSeverities: [critical],
2424
- alertSeverities: [],
2425
- });
2426
-
2427
- const statuses: Array<Record<string, unknown>> = statusesOf(
2428
- await readCompliance(),
2429
- );
2430
-
2431
- expect(statuses[0]!["isCompliant"]).toBe(false);
2432
- expect(statuses[0]!["nonCompliantRules"]).toEqual([
2433
- {
2434
- ruleType: ComplianceRuleType.HasIncidentOnCallRules,
2435
- reason: "Missing notification rules for incident severities: Critical",
2436
- },
2437
- ]);
2438
- });
2439
-
2440
- test("DEFECT closed: an alert rule does not satisfy an incident severity of the same id", async () => {
2441
- /*
2442
- * The sharpest form of the ruleType fix. One rule row, one severity id, and
2443
- * the two checks disagree about it - because the severity is taken from the
2444
- * column the RULE TYPE dictates rather than from whichever one happens to be
2445
- * populated.
2446
- */
2447
- stage({
2448
- settings: [
2449
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2450
- { ruleType: ComplianceRuleType.HasAlertOnCallRules, enabled: true },
2451
- ],
2452
- members: [ada],
2453
- rules: [
2454
- {
2455
- _id: "rule-alert-only",
2456
- userId: ada.id,
2457
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
2458
- alertSeverityId: page.id,
2459
- userEmailId: ObjectID.generate(),
2460
- },
2461
- ],
2462
- incidentSeverities: [critical],
2463
- alertSeverities: [page],
2464
- });
2465
-
2466
- const statuses: Array<Record<string, unknown>> = statusesOf(
2467
- await readCompliance(),
2468
- );
2469
-
2470
- expect(statuses[0]!["nonCompliantRules"]).toEqual([
2471
- {
2472
- ruleType: ComplianceRuleType.HasIncidentOnCallRules,
2473
- reason: "Missing notification rules for incident severities: Critical",
2474
- },
2475
- ]);
2476
- });
2477
-
2478
- test("a legacy rule row with a NULL isOptOut still counts as coverage", async () => {
2479
- /*
2480
- * isOptOut is nullable and was added long after these rows started
2481
- * existing, so it is NULL on every rule in every existing install. An
2482
- * implementation that classified coverage with `isOptOut === false` would
2483
- * match none of them and report a fully-configured project as entirely
2484
- * unready - which is why the split is `isOptOut === true`, the exact dual of
2485
- * the notInOrNull predicate the paging path uses.
2486
- */
2487
- stage({
2488
- settings: [
2489
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2490
- ],
2491
- members: [ada],
2492
- rules: [
2493
- {
2494
- _id: "rule-legacy",
2495
- userId: ada.id,
2496
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2497
- incidentSeverityId: critical.id,
2498
- isOptOut: undefined,
2499
- userEmailId: ObjectID.generate(),
2500
- },
2501
- ],
2502
- incidentSeverities: [critical],
2503
- alertSeverities: [],
2504
- });
2505
-
2506
- const statuses: Array<Record<string, unknown>> = statusesOf(
2507
- await readCompliance(),
2508
- );
2509
-
2510
- expect(statuses[0]!["isCompliant"]).toBe(true);
2511
- });
2061
+ describe("the readiness read routes - a refused caller reads nothing", () => {
2062
+ test.each<[string]>([[POLICY_ROUTE], [PROJECT_ROUTE], [USER_ROUTE]])(
2063
+ "%s",
2064
+ async (uri: string) => {
2065
+ propsSpy.mockResolvedValue({} as never);
2512
2066
 
2513
- test("an explicitly opted-out severity is coverage, not a gap", async () => {
2514
- stage({
2515
- settings: [
2516
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2517
- ],
2518
- members: [ada],
2519
- rules: [
2520
- {
2521
- _id: "rule-opt-out",
2522
- userId: ada.id,
2523
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2524
- incidentSeverityId: critical.id,
2525
- isOptOut: true,
2067
+ const result: RouteCallResult = await callGetRoute({
2068
+ uri,
2069
+ params: {
2070
+ policyId: policyId.toString(),
2071
+ userId: subjectUserId.toString(),
2526
2072
  },
2527
- ],
2528
- incidentSeverities: [critical],
2529
- alertSeverities: [],
2530
- });
2531
-
2532
- const statuses: Array<Record<string, unknown>> = statusesOf(
2533
- await readCompliance(),
2534
- );
2535
-
2536
- // Deliberate silence is not a compliance failure.
2537
- expect(statuses[0]!["isCompliant"]).toBe(true);
2538
- });
2539
-
2540
- test("DEFECT closed: not one read in the whole render carries the literal limit 100", async () => {
2541
- /*
2542
- * The old code capped team members, users and alert severities at 100 while
2543
- * capping incident severities at LIMIT_PER_PROJECT. Truncation is the worst
2544
- * failure mode a compliance page has: the 101st member was not reported
2545
- * non-compliant, they were simply absent, and an absent row reads as "no
2546
- * problem here". Sweeping every read the render makes - the compliance
2547
- * service's three and the readiness service's dozen - is the assertion that
2548
- * cannot be satisfied by fixing three call sites and missing a fourth.
2549
- */
2550
- stage({
2551
- settings: [
2552
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2553
- { ruleType: ComplianceRuleType.HasAlertOnCallRules, enabled: true },
2554
- ],
2555
- members: [ada, grace],
2556
- rules: [],
2557
- incidentSeverities: [critical, major],
2558
- alertSeverities: [page],
2559
- });
2560
-
2561
- await readCompliance();
2073
+ });
2562
2074
 
2563
- let readsInspected: number = 0;
2075
+ expect(result.nextCallCount).toBe(1);
2076
+ expect(result.thrownToNext).toBeDefined();
2564
2077
 
2565
- for (const spy of everyFindBySpy()) {
2566
- for (const call of callsOf(spy)) {
2567
- readsInspected++;
2568
- expect(call.limit).not.toBe(100);
2078
+ for (const spy of [...everyFindBySpy(), projectFindOneById]) {
2079
+ expect(spy).not.toHaveBeenCalled();
2569
2080
  }
2570
- }
2571
-
2572
- // Guards the guard: an assertion over zero reads proves nothing.
2573
- expect(readsInspected).toBeGreaterThan(10);
2574
-
2575
- expect(firstCall(complianceSettingFindBy).limit).toBe(LIMIT_PER_PROJECT);
2576
- expect(firstCall(teamMemberFindBy).limit).toBe(LIMIT_PER_PROJECT);
2577
- expect(firstCall(userFindBy).limit).toBe(LIMIT_PER_PROJECT);
2578
- // Both severity kinds, which is where the two halves used to disagree.
2579
- expect(firstCall(incidentSeverityFindBy).limit).toBe(LIMIT_PER_PROJECT);
2580
- expect(firstCall(alertSeverityFindBy).limit).toBe(LIMIT_PER_PROJECT);
2581
- expect(firstCall(notificationRuleFindBy).limit).toBe(LIMIT_PER_PROJECT);
2582
- });
2583
-
2584
- test("DEFECT closed: the read count does not grow with members or severities", async () => {
2585
- /*
2586
- * The N+1. The old service issued one findBy per severity per user, so a
2587
- * team of 20 in a project with 5 severities cost 100 round trips to render
2588
- * one page. The proof is comparative rather than absolute: the same render
2589
- * with three times the members and three times the severities must cost the
2590
- * same number of reads, whatever that number happens to be.
2591
- */
2592
- stage({
2593
- settings: [
2594
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2595
- ],
2596
- members: [ada],
2597
- rules: [],
2598
- incidentSeverities: [critical],
2599
- alertSeverities: [],
2600
- });
2601
-
2602
- await readCompliance();
2603
-
2604
- const smallRuleReads: number = notificationRuleFindBy.mock.calls.length;
2605
- const smallUserReads: number = userFindBy.mock.calls.length;
2606
- const smallSeverityReads: number =
2607
- incidentSeverityFindBy.mock.calls.length +
2608
- alertSeverityFindBy.mock.calls.length;
2609
-
2610
- expect(smallRuleReads).toBe(1);
2611
-
2612
- /*
2613
- * mockClear, not mockReset: the stubs' resolved values are implementations
2614
- * and have to survive into the second render.
2615
- */
2616
- jest.clearAllMocks();
2617
- OnCallReadinessService.clearCache();
2618
-
2619
- const carol: StubUser = {
2620
- id: ObjectID.generate(),
2621
- name: "Carol",
2622
- email: "carol@example.com",
2623
- };
2624
- const minor: StubSeverity = { id: ObjectID.generate(), name: "Minor" };
2625
- const trivial: StubSeverity = { id: ObjectID.generate(), name: "Trivial" };
2626
-
2627
- stage({
2628
- settings: [
2629
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2630
- ],
2631
- members: [ada, grace, carol],
2632
- rules: [],
2633
- incidentSeverities: [critical, major, minor, trivial],
2634
- alertSeverities: [page],
2635
- });
2636
-
2637
- await readCompliance();
2638
-
2639
- expect(notificationRuleFindBy.mock.calls.length).toBe(smallRuleReads);
2640
- expect(userFindBy.mock.calls.length).toBe(smallUserReads);
2641
- expect(
2642
- incidentSeverityFindBy.mock.calls.length +
2643
- alertSeverityFindBy.mock.calls.length,
2644
- ).toBe(smallSeverityReads);
2645
- });
2646
-
2647
- test("one notification-rule read covers every member, batched on userId", async () => {
2648
- stage({
2649
- settings: [
2650
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: true },
2651
- ],
2652
- members: [ada, grace],
2653
- rules: [
2654
- {
2655
- _id: "rule-ada",
2656
- userId: ada.id,
2657
- ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2658
- incidentSeverityId: critical.id,
2659
- userEmailId: ObjectID.generate(),
2660
- },
2661
- ],
2662
- incidentSeverities: [critical],
2663
- alertSeverities: [],
2664
- });
2665
-
2666
- const statuses: Array<Record<string, unknown>> = statusesOf(
2667
- await readCompliance(),
2668
- );
2669
-
2670
- expect(notificationRuleFindBy).toHaveBeenCalledTimes(1);
2671
-
2672
- const call: CapturedFindBy = firstCall(notificationRuleFindBy);
2673
- // One query, scoped to the project, listing every user it is asking about.
2674
- expect(call.query["userId"]).toBeDefined();
2675
- expect(call.props?.isRoot).toBe(true);
2676
-
2677
- /*
2678
- * And the batched read still separates the two people: Ada's rule must not
2679
- * cover Grace. A batched query folded into a per-user map is exactly where
2680
- * that mistake would hide.
2681
- */
2682
- expect(statuses[0]!["userName"]).toBe("Ada");
2683
- expect(statuses[0]!["isCompliant"]).toBe(true);
2684
- expect(statuses[1]!["userName"]).toBe("Grace");
2685
- expect(statuses[1]!["isCompliant"]).toBe(false);
2686
- });
2687
-
2688
- test("readiness is not computed at all when no on-call rule is enabled", async () => {
2689
- /*
2690
- * The four channel rules do not consult readiness, so a team that only
2691
- * checks "has a verified email" must not pay for a project-wide readiness
2692
- * pass on every render.
2693
- */
2694
- stage({
2695
- settings: [
2696
- {
2697
- ruleType: ComplianceRuleType.HasNotificationEmailMethod,
2698
- enabled: true,
2699
- },
2700
- { ruleType: ComplianceRuleType.HasIncidentOnCallRules, enabled: false },
2701
- ],
2702
- members: [ada],
2703
- rules: [],
2704
- incidentSeverities: [critical],
2705
- alertSeverities: [],
2706
- });
2707
-
2708
- await readCompliance();
2709
-
2710
- expect(notificationRuleFindBy).not.toHaveBeenCalled();
2711
- expect(incidentSeverityFindBy).not.toHaveBeenCalled();
2712
- expect(escalationUserFindBy).not.toHaveBeenCalled();
2713
- expect(projectFindOneById).not.toHaveBeenCalled();
2714
- // The channel rule it WAS asked about still runs.
2715
- expect(userEmailFindBy).toHaveBeenCalledTimes(1);
2716
- });
2717
-
2718
- test("a team that does not exist is refused rather than described", async () => {
2719
- /*
2720
- * Refused by the ROUTE now, before the service is reached, and with the same
2721
- * words a team in another project gets - see the authorisation block below
2722
- * for why those two cases must be indistinguishable. The service refuses it
2723
- * as well, on its own, with a BadDataException; that guard is pinned in
2724
- * TeamComplianceServiceBehaviour.test.ts because it protects in-process
2725
- * callers who never come through this route.
2726
- */
2727
- teamFindOneById.mockResolvedValue(null as never);
2728
-
2729
- const result: RouteCallResult = await callGetRoute({
2730
- uri: COMPLIANCE_ROUTE,
2731
- params: { teamId: teamId.toString() },
2732
- });
2733
-
2734
- expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
2735
- expect((result.thrownToNext as NotAuthorizedException).message).toBe(
2736
- REFUSAL,
2737
- );
2738
- expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
2739
- });
2740
- });
2741
-
2742
- /*
2743
- * --------------------------------------------------------------------------- *
2744
- * GET /team/compliance-status/:teamId - authorisation.
2745
- *
2746
- * PRE-EXISTING HOLE, not a Phase 2 regression, closed here because Phase 2
2747
- * rebuilt what sits behind this route.
2748
- *
2749
- * The route was mounted with UserMiddleware.getUserMiddleware - which admits an
2750
- * anonymous request as UserType.Public and calls next() - and then checked only
2751
- * that `databaseProps.tenantId` was non-empty. tenantId comes from a
2752
- * caller-supplied `tenantid` header. Everything underneath reads with
2753
- * isRoot: true. So a `tenantid` header and a team id were the entire
2754
- * authentication story for "which of these named people cannot be paged", which
2755
- * is a roster of who to phone during an outage and who will never pick up.
2756
- *
2757
- * Worse in combination: the service read the team by id with no projectId in the
2758
- * query, so the team did not even have to belong to the project in the header.
2759
- *
2760
- * Both halves are tested here - the caller has to be a member, AND the team has
2761
- * to be theirs - and the refusals are asserted to be word-for-word identical so
2762
- * the route cannot be used to enumerate team ids across tenants.
2763
- * ---------------------------------------------------------------------------
2764
- */
2765
-
2766
- describe("GET /team/compliance-status/:teamId - authorisation", () => {
2767
- let complianceSpy: jest.SpyInstance;
2768
-
2769
- beforeEach(() => {
2770
- complianceSpy = jest.spyOn(
2771
- TeamComplianceService,
2772
- "getTeamComplianceStatus",
2773
- );
2774
- });
2775
-
2776
- // Was a BadDataException (400) before the credential check.
2777
- test("refuses an unauthenticated caller with 401 before reading anything", async () => {
2778
- propsSpy.mockResolvedValue({} as never);
2779
-
2780
- const result: RouteCallResult = await callGetRoute({
2781
- uri: COMPLIANCE_ROUTE,
2782
- params: { teamId: teamId.toString() },
2783
- });
2784
-
2785
- expectAuthenticationRequired(result.thrownToNext);
2786
- expect(teamFindOneById).not.toHaveBeenCalled();
2787
- expect(complianceSpy).not.toHaveBeenCalled();
2788
- });
2789
-
2790
- test("refuses a public caller that merely supplies a tenantid header with 401", async () => {
2791
- /*
2792
- * THE hole, in its exact shape: the request getUserMiddleware produces for
2793
- * an anonymous caller who sent a `tenantid` header. A project id, no user,
2794
- * no permissions - and, until this change, a complete compliance report.
2795
- *
2796
- * Refused as unauthenticated (401), not with the member refusal it used to
2797
- * get: this is also what a signed-in admin's request looks like once the
2798
- * access-token cookie has expired, and only a 401 makes the dashboard
2799
- * refresh the session and resend.
2800
- */
2801
- propsSpy.mockResolvedValue({
2802
- tenantId: projectId,
2803
- userId: undefined,
2804
- userTenantAccessPermission: undefined,
2805
- } as never);
2806
-
2807
- const result: RouteCallResult = await callGetRoute({
2808
- uri: COMPLIANCE_ROUTE,
2809
- params: { teamId: teamId.toString() },
2810
- });
2811
-
2812
- expectAuthenticationRequired(result.thrownToNext);
2813
- expect(teamFindOneById).not.toHaveBeenCalled();
2814
- expect(complianceSpy).not.toHaveBeenCalled();
2815
- });
2816
-
2817
- test("refuses a logged-in caller whose tenantid names a project they are not in", async () => {
2818
- propsSpy.mockResolvedValue({
2819
- tenantId: projectId,
2820
- userId: callerUserId,
2821
- // Logged in, but with no permission entry for the project they named.
2822
- userTenantAccessPermission: {},
2823
- } as never);
2824
-
2825
- const result: RouteCallResult = await callGetRoute({
2826
- uri: COMPLIANCE_ROUTE,
2827
- params: { teamId: teamId.toString() },
2828
- });
2829
-
2830
- // Authenticated, so the refusal keeps its 422 - only anonymity is a 401.
2831
- expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
2832
- expect((result.thrownToNext as Exception).code).toBe(
2833
- ExceptionCode.NotAuthorizedException,
2834
- );
2835
- expect(complianceSpy).not.toHaveBeenCalled();
2836
- });
2837
-
2838
- test("a member of one project cannot read another project's team", async () => {
2839
- /*
2840
- * The caller is a real member of their own project; only the team id is
2841
- * borrowed. Without the resource check this is the whole exploit - every
2842
- * read underneath runs as root, so the borrowed id resolves and the report
2843
- * is rendered for a team the caller has nothing to do with.
2844
- */
2845
- teamFindOneById.mockResolvedValue({
2846
- _id: teamId.toString(),
2847
- projectId: otherProjectId,
2848
- } as never);
2849
-
2850
- const result: RouteCallResult = await callGetRoute({
2851
- uri: COMPLIANCE_ROUTE,
2852
- params: { teamId: teamId.toString() },
2853
- });
2854
-
2855
- expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
2856
- expect((result.thrownToNext as NotAuthorizedException).message).toBe(
2857
- REFUSAL,
2858
- );
2859
- expect(complianceSpy).not.toHaveBeenCalled();
2860
- expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
2861
- });
2862
-
2863
- test("a foreign team and an unknown team are refused in identical words", async () => {
2864
- teamFindOneById.mockResolvedValue({
2865
- _id: teamId.toString(),
2866
- projectId: otherProjectId,
2867
- } as never);
2868
- const foreign: RouteCallResult = await callGetRoute({
2869
- uri: COMPLIANCE_ROUTE,
2870
- params: { teamId: teamId.toString() },
2871
- });
2872
-
2873
- teamFindOneById.mockResolvedValue(null as never);
2874
- const unknown: RouteCallResult = await callGetRoute({
2875
- uri: COMPLIANCE_ROUTE,
2876
- params: { teamId: ObjectID.generate().toString() },
2877
- });
2878
-
2879
- expect((foreign.thrownToNext as NotAuthorizedException).message).toBe(
2880
- (unknown.thrownToNext as NotAuthorizedException).message,
2881
- );
2882
- expect((foreign.thrownToNext as NotAuthorizedException).message).toBe(
2883
- REFUSAL,
2884
- );
2885
- });
2886
-
2887
- test("a team row carrying no projectId at all is refused, not trusted", async () => {
2888
- // The shape a `select` that forgot projectId would produce.
2889
- teamFindOneById.mockResolvedValue({ _id: teamId.toString() } as never);
2890
-
2891
- const result: RouteCallResult = await callGetRoute({
2892
- uri: COMPLIANCE_ROUTE,
2893
- params: { teamId: teamId.toString() },
2894
- });
2895
-
2896
- expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
2897
- expect(complianceSpy).not.toHaveBeenCalled();
2898
- });
2899
-
2900
- test("a malformed team id is rejected as bad data, not turned into a query", async () => {
2901
- /*
2902
- * The old handler built `new ObjectID(req.params.teamId)` from any string
2903
- * and then tested it for truthiness - which an ObjectID always is - so a
2904
- * malformed id travelled all the way to a query that matched nothing and
2905
- * came back as "this team does not exist".
2906
- */
2907
- const result: RouteCallResult = await callGetRoute({
2908
- uri: COMPLIANCE_ROUTE,
2909
- params: { teamId: "not-a-uuid" },
2910
- });
2911
-
2912
- expect(result.thrownToNext).toBeInstanceOf(BadDataException);
2913
- expect(teamFindOneById).not.toHaveBeenCalled();
2914
- expect(complianceSpy).not.toHaveBeenCalled();
2915
- });
2916
-
2917
- test("the authorised path reads the team's own projectId, as root, and proceeds", async () => {
2918
- const result: RouteCallResult = await callGetRoute({
2919
- uri: COMPLIANCE_ROUTE,
2920
- params: { teamId: teamId.toString() },
2921
- });
2922
-
2923
- expect(result.nextCallCount).toBe(0);
2924
-
2925
- const read: {
2926
- id: ObjectID;
2927
- select: Record<string, unknown>;
2928
- props: { isRoot?: boolean | undefined };
2929
- } = teamFindOneById.mock.calls[0]![0] as {
2930
- id: ObjectID;
2931
- select: Record<string, unknown>;
2932
- props: { isRoot?: boolean | undefined };
2933
- };
2934
-
2935
- expect(read.id.toString()).toBe(teamId.toString());
2936
- expect(read.select["projectId"]).toBe(true);
2937
- expect(read.props.isRoot).toBe(true);
2938
-
2939
- // And the service is asked about the project the CALLER was authorised for.
2940
- expect(complianceSpy).toHaveBeenCalledTimes(1);
2941
- expect(complianceSpy.mock.calls[0]![1]).toEqual(projectId);
2942
-
2943
- /*
2944
- * Underneath, the service scopes its OWN team read to id and project
2945
- * together. The route's check and this one are not redundant: the route's
2946
- * answers an authorisation question (403, and only for HTTP callers), this
2947
- * one is a data-scoping guard that also covers the on-call banner and any
2948
- * other in-process caller that never passes through the route at all.
2949
- */
2950
- const scoped: CapturedFindBy = firstCall(teamFindOneBy);
2951
- expect(scoped.query["_id"]).toBe(teamId.toString());
2952
- expect((scoped.query["projectId"] as ObjectID).toString()).toBe(
2953
- projectId.toString(),
2954
- );
2955
- expect(scoped.props?.isRoot).toBe(true);
2956
- });
2081
+ },
2082
+ );
2957
2083
  });