@oneuptime/common 12.0.17 → 12.0.19

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 (220) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  11. package/Server/API/UserSlackAPI.ts +125 -0
  12. package/Server/EnvironmentConfig.ts +12 -107
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Infrastructure/Queue.ts +7 -0
  19. package/Server/Services/BillingService.ts +8 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +67 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  40. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  41. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  42. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  43. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  44. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
  45. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  46. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  47. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  48. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  49. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  50. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  51. package/Tests/Server/Services/BillingService.test.ts +20 -0
  52. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  53. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  54. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  55. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  56. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  57. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  58. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  59. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  60. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  61. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  62. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  63. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  64. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  65. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  66. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  67. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  68. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  69. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  70. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  71. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  72. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  73. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  74. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  75. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  76. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  77. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  78. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  79. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  80. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  81. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  82. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  83. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  84. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  85. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  86. package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
  87. package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
  88. package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
  90. package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
  91. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  92. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  93. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
  94. package/Types/Billing/PayAsYouGoPricing.ts +1 -1
  95. package/Types/Marketing/MarketingEvent.ts +97 -0
  96. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  97. package/UI/Utils/PermissionGate.ts +56 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  99. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  101. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  103. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  105. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  106. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  107. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  109. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  111. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  113. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  114. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  115. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  116. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  117. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  118. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  119. package/build/dist/Server/EnvironmentConfig.js +10 -70
  120. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  131. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  132. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  133. package/build/dist/Server/Services/BillingService.js +6 -0
  134. package/build/dist/Server/Services/BillingService.js.map +1 -1
  135. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  136. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  137. package/build/dist/Server/Services/Index.js +6 -2
  138. package/build/dist/Server/Services/Index.js.map +1 -1
  139. package/build/dist/Server/Services/LlmLogService.js +25 -9
  140. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  141. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  142. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  143. package/build/dist/Server/Services/ProjectService.js +56 -0
  144. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  145. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  146. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  147. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  148. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  149. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  150. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  151. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  152. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  153. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  154. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  155. package/build/dist/Server/Services/UserService.js +26 -1
  156. package/build/dist/Server/Services/UserService.js.map +1 -1
  157. package/build/dist/Server/Services/UserSlackService.js +195 -0
  158. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  159. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  160. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  161. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  162. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  163. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  164. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  165. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  166. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  167. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  168. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  169. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  170. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  171. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  172. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  173. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  174. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  175. package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
  176. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
  177. package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
  178. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  179. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  180. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  181. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  182. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  183. package/package.json +1 -1
  184. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  185. package/Server/Services/MarketingConversionService.ts +0 -10
  186. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  187. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  188. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  189. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  190. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  191. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  192. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  193. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  194. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  195. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  196. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  197. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  198. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  199. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  200. package/Types/Marketing/MarketingConversion.ts +0 -53
  201. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  202. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  203. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  204. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  205. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  206. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  207. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  208. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  209. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  210. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  211. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  212. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  213. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  214. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  215. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  216. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  217. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  218. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  219. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  220. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,228 @@
1
+ import {
2
+ RUNBOOK_ADVANCE_PERMISSIONS,
3
+ RUNBOOK_EXECUTE_PERMISSIONS,
4
+ assertCanAdvanceRunbookExecutions,
5
+ assertCanExecuteRunbooks,
6
+ } from "../../../../Server/Utils/Runbook/RunbookExecutePermission";
7
+ import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
8
+ import NotAuthorizedException from "../../../../Types/Exception/NotAuthorizedException";
9
+ import ObjectID from "../../../../Types/ObjectID";
10
+ import Permission, {
11
+ UserPermission,
12
+ UserTenantAccessPermission,
13
+ } from "../../../../Types/Permission";
14
+
15
+ /*
16
+ * These are the pure authorization gates that stand in for RunbookExecution's
17
+ * create/update ACLs on the runbook-execution routes. The tests below pin the
18
+ * three things that are easy to regress by hand-editing the permission lists:
19
+ * that an allowed permission grants, that a BLOCK row never grants, and that a
20
+ * grant in one project never leaks into another.
21
+ */
22
+
23
+ const PROJECT_ID: ObjectID = new ObjectID("60f7d9b0a1b2c3d4e5f60001");
24
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID("60f7d9b0a1b2c3d4e5f60002");
25
+
26
+ function userPermission(
27
+ permission: Permission,
28
+ overrides?: Partial<UserPermission>,
29
+ ): UserPermission {
30
+ return {
31
+ _type: "UserPermission",
32
+ permission,
33
+ labelIds: [],
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ function propsWith(
39
+ permissions: Array<UserPermission>,
40
+ projectId: ObjectID = PROJECT_ID,
41
+ ): DatabaseCommonInteractionProps {
42
+ const tenantPermission: UserTenantAccessPermission = {
43
+ _type: "UserTenantAccessPermission",
44
+ projectId,
45
+ permissions,
46
+ };
47
+
48
+ return {
49
+ userTenantAccessPermission: {
50
+ [projectId.toString()]: tenantPermission,
51
+ },
52
+ };
53
+ }
54
+
55
+ describe("RunbookExecutePermission", () => {
56
+ describe("permission list invariants", () => {
57
+ test("execute list contains RunbookExecution's create ACL roles", () => {
58
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).toContain(Permission.ProjectOwner);
59
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).toContain(Permission.ProjectAdmin);
60
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).toContain(Permission.ProjectMember);
61
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).toContain(
62
+ Permission.CreateRunbookExecution,
63
+ );
64
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).toContain(Permission.RunbookAdmin);
65
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).toContain(Permission.RunbookMember);
66
+ });
67
+
68
+ test("advance list is a strict superset of the execute list", () => {
69
+ for (const permission of RUNBOOK_EXECUTE_PERMISSIONS) {
70
+ expect(RUNBOOK_ADVANCE_PERMISSIONS).toContain(permission);
71
+ }
72
+ expect(RUNBOOK_ADVANCE_PERMISSIONS).toContain(
73
+ Permission.EditRunbookExecution,
74
+ );
75
+ expect(RUNBOOK_ADVANCE_PERMISSIONS.length).toBe(
76
+ RUNBOOK_EXECUTE_PERMISSIONS.length + 1,
77
+ );
78
+ });
79
+
80
+ test("EditRunbookExecution advances but does not start executions", () => {
81
+ // It is documented as the tick-off-without-starting permission.
82
+ expect(RUNBOOK_ADVANCE_PERMISSIONS).toContain(
83
+ Permission.EditRunbookExecution,
84
+ );
85
+ expect(RUNBOOK_EXECUTE_PERMISSIONS).not.toContain(
86
+ Permission.EditRunbookExecution,
87
+ );
88
+ });
89
+ });
90
+
91
+ describe("assertCanExecuteRunbooks", () => {
92
+ test.each(RUNBOOK_EXECUTE_PERMISSIONS)(
93
+ "grants when the user holds %s",
94
+ (permission: Permission) => {
95
+ const props: DatabaseCommonInteractionProps = propsWith([
96
+ userPermission(permission),
97
+ ]);
98
+
99
+ expect(() => {
100
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
101
+ }).not.toThrow();
102
+ },
103
+ );
104
+
105
+ test("denies when the user holds no runbook permission", () => {
106
+ const props: DatabaseCommonInteractionProps = propsWith([
107
+ userPermission(Permission.MonitorViewer),
108
+ ]);
109
+
110
+ expect(() => {
111
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
112
+ }).toThrow(NotAuthorizedException);
113
+ });
114
+
115
+ test("denies when the only matching row is a block permission", () => {
116
+ const props: DatabaseCommonInteractionProps = propsWith([
117
+ userPermission(Permission.ProjectOwner, { isBlockPermission: true }),
118
+ ]);
119
+
120
+ expect(() => {
121
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
122
+ }).toThrow(NotAuthorizedException);
123
+ });
124
+
125
+ test("EditRunbookExecution alone cannot start an execution", () => {
126
+ const props: DatabaseCommonInteractionProps = propsWith([
127
+ userPermission(Permission.EditRunbookExecution),
128
+ ]);
129
+
130
+ expect(() => {
131
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
132
+ }).toThrow(NotAuthorizedException);
133
+ });
134
+
135
+ test("denies when there is no tenant access permission at all", () => {
136
+ expect(() => {
137
+ return assertCanExecuteRunbooks({}, PROJECT_ID);
138
+ }).toThrow(NotAuthorizedException);
139
+ });
140
+
141
+ test("denies when the permission set is empty", () => {
142
+ const props: DatabaseCommonInteractionProps = propsWith([]);
143
+
144
+ expect(() => {
145
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
146
+ }).toThrow(NotAuthorizedException);
147
+ });
148
+
149
+ test("a grant in another project does not leak into this one", () => {
150
+ // User is a ProjectOwner in OTHER_PROJECT_ID but not in PROJECT_ID.
151
+ const props: DatabaseCommonInteractionProps = propsWith(
152
+ [userPermission(Permission.ProjectOwner)],
153
+ OTHER_PROJECT_ID,
154
+ );
155
+
156
+ expect(() => {
157
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
158
+ }).toThrow(NotAuthorizedException);
159
+ });
160
+
161
+ test("uses the start-runbook denial message", () => {
162
+ expect(() => {
163
+ return assertCanExecuteRunbooks(propsWith([]), PROJECT_ID);
164
+ }).toThrow("You do not have permission to start runbook executions");
165
+ });
166
+
167
+ test("an allow row still grants alongside an unrelated block row", () => {
168
+ const props: DatabaseCommonInteractionProps = propsWith([
169
+ userPermission(Permission.MonitorViewer, { isBlockPermission: true }),
170
+ userPermission(Permission.RunbookMember),
171
+ ]);
172
+
173
+ expect(() => {
174
+ return assertCanExecuteRunbooks(props, PROJECT_ID);
175
+ }).not.toThrow();
176
+ });
177
+ });
178
+
179
+ describe("assertCanAdvanceRunbookExecutions", () => {
180
+ test("grants for EditRunbookExecution (advance-only permission)", () => {
181
+ const props: DatabaseCommonInteractionProps = propsWith([
182
+ userPermission(Permission.EditRunbookExecution),
183
+ ]);
184
+
185
+ expect(() => {
186
+ return assertCanAdvanceRunbookExecutions(props, PROJECT_ID);
187
+ }).not.toThrow();
188
+ });
189
+
190
+ test("grants for a RunbookMember who could also start the run", () => {
191
+ const props: DatabaseCommonInteractionProps = propsWith([
192
+ userPermission(Permission.RunbookMember),
193
+ ]);
194
+
195
+ expect(() => {
196
+ return assertCanAdvanceRunbookExecutions(props, PROJECT_ID);
197
+ }).not.toThrow();
198
+ });
199
+
200
+ test("denies when the user holds neither execute nor edit rights", () => {
201
+ const props: DatabaseCommonInteractionProps = propsWith([
202
+ userPermission(Permission.MonitorViewer),
203
+ ]);
204
+
205
+ expect(() => {
206
+ return assertCanAdvanceRunbookExecutions(props, PROJECT_ID);
207
+ }).toThrow(NotAuthorizedException);
208
+ });
209
+
210
+ test("denies when the matching row is a block permission", () => {
211
+ const props: DatabaseCommonInteractionProps = propsWith([
212
+ userPermission(Permission.EditRunbookExecution, {
213
+ isBlockPermission: true,
214
+ }),
215
+ ]);
216
+
217
+ expect(() => {
218
+ return assertCanAdvanceRunbookExecutions(props, PROJECT_ID);
219
+ }).toThrow(NotAuthorizedException);
220
+ });
221
+
222
+ test("uses the change-runbook denial message", () => {
223
+ expect(() => {
224
+ return assertCanAdvanceRunbookExecutions(propsWith([]), PROJECT_ID);
225
+ }).toThrow("You do not have permission to change runbook executions");
226
+ });
227
+ });
228
+ });
@@ -0,0 +1,189 @@
1
+ /*
2
+ * The erasure tombstone is the only thing that stops a session-replay chunk
3
+ * staged in Redis before a GDPR/CCPA erasure from being written back into
4
+ * ClickHouse after it. Its whole contract is "fail closed": a Redis blip must
5
+ * never be read as "not erased", and an erasure must never proceed without a
6
+ * tombstone the ingest path can later consult. These tests pin exactly that.
7
+ */
8
+
9
+ jest.mock("../../../../Server/Infrastructure/Redis", () => {
10
+ return {
11
+ __esModule: true,
12
+ default: {
13
+ getClient: jest.fn(),
14
+ isConnected: jest.fn(),
15
+ },
16
+ };
17
+ });
18
+
19
+ jest.mock("../../../../Server/Utils/Logger", () => {
20
+ return {
21
+ __esModule: true,
22
+ default: {
23
+ debug: jest.fn(),
24
+ info: jest.fn(),
25
+ warn: jest.fn(),
26
+ error: jest.fn(),
27
+ },
28
+ };
29
+ });
30
+
31
+ import Redis from "../../../../Server/Infrastructure/Redis";
32
+ import {
33
+ ERASURE_TOMBSTONE_TTL_SECONDS,
34
+ ErasureTombstoneUnavailableError,
35
+ getErasedSessionsKey,
36
+ isSessionErased,
37
+ writeErasureTombstones,
38
+ } from "../../../../Server/Utils/SessionReplay/SessionReplayErasureTombstone";
39
+
40
+ const getClientMock: jest.Mock = Redis.getClient as unknown as jest.Mock;
41
+ const isConnectedMock: jest.Mock = Redis.isConnected as unknown as jest.Mock;
42
+
43
+ interface FakeClient {
44
+ sadd: jest.Mock;
45
+ expire: jest.Mock;
46
+ sismember: jest.Mock;
47
+ }
48
+
49
+ function connectedClient(overrides?: Partial<FakeClient>): FakeClient {
50
+ const client: FakeClient = {
51
+ sadd: jest.fn().mockResolvedValue(1),
52
+ expire: jest.fn().mockResolvedValue(1),
53
+ sismember: jest.fn().mockResolvedValue(0),
54
+ ...overrides,
55
+ };
56
+ getClientMock.mockReturnValue(client);
57
+ isConnectedMock.mockReturnValue(true);
58
+ return client;
59
+ }
60
+
61
+ function disconnected(): void {
62
+ getClientMock.mockReturnValue(null);
63
+ isConnectedMock.mockReturnValue(false);
64
+ }
65
+
66
+ describe("SessionReplayErasureTombstone", () => {
67
+ beforeEach(() => {
68
+ jest.clearAllMocks();
69
+ });
70
+
71
+ describe("getErasedSessionsKey", () => {
72
+ test("is namespaced per project", () => {
73
+ expect(getErasedSessionsKey("proj-1")).toBe("replay:erased:proj-1");
74
+ expect(getErasedSessionsKey("proj-2")).toBe("replay:erased:proj-2");
75
+ expect(getErasedSessionsKey("proj-1")).not.toBe(
76
+ getErasedSessionsKey("proj-2"),
77
+ );
78
+ });
79
+ });
80
+
81
+ describe("ERASURE_TOMBSTONE_TTL_SECONDS", () => {
82
+ test("is seven days", () => {
83
+ expect(ERASURE_TOMBSTONE_TTL_SECONDS).toBe(7 * 24 * 60 * 60);
84
+ });
85
+ });
86
+
87
+ describe("writeErasureTombstones", () => {
88
+ test("no-ops on an empty session list without touching Redis", async () => {
89
+ const client: FakeClient = connectedClient();
90
+
91
+ await writeErasureTombstones({ projectId: "p1", sessionIds: [] });
92
+
93
+ expect(client.sadd).not.toHaveBeenCalled();
94
+ expect(client.expire).not.toHaveBeenCalled();
95
+ });
96
+
97
+ test("adds the sessions to the per-project set and refreshes the TTL", async () => {
98
+ const client: FakeClient = connectedClient();
99
+
100
+ await writeErasureTombstones({
101
+ projectId: "p1",
102
+ sessionIds: ["s1", "s2"],
103
+ });
104
+
105
+ expect(client.sadd).toHaveBeenCalledWith("replay:erased:p1", [
106
+ "s1",
107
+ "s2",
108
+ ]);
109
+ expect(client.expire).toHaveBeenCalledWith(
110
+ "replay:erased:p1",
111
+ ERASURE_TOMBSTONE_TTL_SECONDS,
112
+ );
113
+ });
114
+
115
+ test("throws when Redis is unavailable so the erasure batch aborts", async () => {
116
+ disconnected();
117
+
118
+ await expect(
119
+ writeErasureTombstones({ projectId: "p1", sessionIds: ["s1"] }),
120
+ ).rejects.toThrow(/Redis is not connected/);
121
+ });
122
+
123
+ test("does not swallow a non-empty write when the client exists but is not connected", async () => {
124
+ getClientMock.mockReturnValue({
125
+ sadd: jest.fn(),
126
+ expire: jest.fn(),
127
+ });
128
+ isConnectedMock.mockReturnValue(false);
129
+
130
+ await expect(
131
+ writeErasureTombstones({ projectId: "p1", sessionIds: ["s1"] }),
132
+ ).rejects.toThrow(/refusing to erase/);
133
+ });
134
+ });
135
+
136
+ describe("isSessionErased", () => {
137
+ test("returns false for an empty session id without hitting Redis", async () => {
138
+ const client: FakeClient = connectedClient();
139
+
140
+ await expect(
141
+ isSessionErased({ projectId: "p1", sessionId: "" }),
142
+ ).resolves.toBe(false);
143
+ expect(client.sismember).not.toHaveBeenCalled();
144
+ });
145
+
146
+ test("returns true when the session is a member of the tombstone set", async () => {
147
+ const client: FakeClient = connectedClient({
148
+ sismember: jest.fn().mockResolvedValue(1),
149
+ });
150
+
151
+ await expect(
152
+ isSessionErased({ projectId: "p1", sessionId: "s1" }),
153
+ ).resolves.toBe(true);
154
+ expect(client.sismember).toHaveBeenCalledWith("replay:erased:p1", "s1");
155
+ });
156
+
157
+ test("returns false when the session is not in the tombstone set", async () => {
158
+ connectedClient({ sismember: jest.fn().mockResolvedValue(0) });
159
+
160
+ await expect(
161
+ isSessionErased({ projectId: "p1", sessionId: "s1" }),
162
+ ).resolves.toBe(false);
163
+ });
164
+
165
+ test("throws ErasureTombstoneUnavailableError when Redis is unavailable (never returns a boolean)", async () => {
166
+ disconnected();
167
+
168
+ await expect(
169
+ isSessionErased({ projectId: "p1", sessionId: "s1" }),
170
+ ).rejects.toBeInstanceOf(ErasureTombstoneUnavailableError);
171
+ });
172
+
173
+ test("throws ErasureTombstoneUnavailableError (not the raw error) when the lookup fails", async () => {
174
+ connectedClient({
175
+ sismember: jest.fn().mockRejectedValue(new Error("connection reset")),
176
+ });
177
+
178
+ const promise: Promise<boolean> = isSessionErased({
179
+ projectId: "p1",
180
+ sessionId: "s1",
181
+ });
182
+
183
+ await expect(promise).rejects.toBeInstanceOf(
184
+ ErasureTombstoneUnavailableError,
185
+ );
186
+ await expect(promise).rejects.toThrow(/connection reset/);
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,205 @@
1
+ import ObjectID from "../../../../Types/ObjectID";
2
+
3
+ /*
4
+ * SessionReplayUsage owns the Redis key names and byte-budget read path shared
5
+ * by the ingest gate (which writes the counters) and the dashboard (which
6
+ * reads them). These tests pin two things that silently break the budget if
7
+ * they drift: the exact key format the two sides must agree on, and the
8
+ * read-path contract that "Redis unavailable" is reported as null (unknown)
9
+ * while "key absent" is reported as 0.
10
+ */
11
+
12
+ jest.mock("../../../../Server/Infrastructure/Redis", () => {
13
+ return {
14
+ __esModule: true,
15
+ default: {
16
+ getClient: jest.fn(),
17
+ isConnected: jest.fn(),
18
+ },
19
+ };
20
+ });
21
+
22
+ jest.mock("../../../../Server/Utils/Logger", () => {
23
+ return {
24
+ __esModule: true,
25
+ default: {
26
+ debug: jest.fn(),
27
+ info: jest.fn(),
28
+ warn: jest.fn(),
29
+ error: jest.fn(),
30
+ },
31
+ };
32
+ });
33
+
34
+ import Redis from "../../../../Server/Infrastructure/Redis";
35
+ import SessionReplayUsage from "../../../../Server/Utils/SessionReplay/SessionReplayUsage";
36
+
37
+ const getClientMock: jest.Mock = Redis.getClient as unknown as jest.Mock;
38
+ const isConnectedMock: jest.Mock = Redis.isConnected as unknown as jest.Mock;
39
+
40
+ const PROJECT_ID: ObjectID = new ObjectID("60f7d9b0a1b2c3d4e5f60001");
41
+ const RUM_APP_ID: ObjectID = new ObjectID("60f7d9b0a1b2c3d4e5f60099");
42
+
43
+ function mockConnectedClient(get: jest.Mock): void {
44
+ getClientMock.mockReturnValue({ get });
45
+ isConnectedMock.mockReturnValue(true);
46
+ }
47
+
48
+ describe("SessionReplayUsage", () => {
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ });
52
+
53
+ describe("UTC bucket helpers", () => {
54
+ test("getUtcDayBucket is a YYYY-MM-DD string", () => {
55
+ const bucket: string = SessionReplayUsage.getUtcDayBucket();
56
+ expect(bucket).toMatch(/^\d{4}-\d{2}-\d{2}$/);
57
+ // Same derivation the implementation promises: the UTC calendar day.
58
+ expect(bucket).toBe(new Date().toISOString().substring(0, 10));
59
+ });
60
+
61
+ test("getUtcMonthBucket is a YYYY-MM string and prefixes the day bucket", () => {
62
+ const month: string = SessionReplayUsage.getUtcMonthBucket();
63
+ expect(month).toMatch(/^\d{4}-\d{2}$/);
64
+ expect(month).toBe(new Date().toISOString().substring(0, 7));
65
+ // The day bucket always starts with the month bucket.
66
+ expect(SessionReplayUsage.getUtcDayBucket().startsWith(month)).toBe(true);
67
+ });
68
+ });
69
+
70
+ describe("key builders", () => {
71
+ test("daily project key embeds prefix, project id and today's UTC day", () => {
72
+ const key: string = SessionReplayUsage.getDailyProjectByteKey(PROJECT_ID);
73
+
74
+ expect(key).toBe(
75
+ `replay:rate:bytes:${PROJECT_ID.toString()}:${SessionReplayUsage.getUtcDayBucket()}`,
76
+ );
77
+ });
78
+
79
+ test("monthly application key embeds project id, rum app id and month", () => {
80
+ const key: string = SessionReplayUsage.getMonthlyApplicationByteKey({
81
+ projectId: PROJECT_ID,
82
+ rumApplicationId: RUM_APP_ID,
83
+ });
84
+
85
+ expect(key).toBe(
86
+ `replay:rate:bytes-month:${PROJECT_ID.toString()}:${RUM_APP_ID.toString()}:${SessionReplayUsage.getUtcMonthBucket()}`,
87
+ );
88
+ });
89
+
90
+ test("daily and monthly keys use distinct prefixes so they never collide", () => {
91
+ const daily: string =
92
+ SessionReplayUsage.getDailyProjectByteKey(PROJECT_ID);
93
+ const monthly: string = SessionReplayUsage.getMonthlyApplicationByteKey({
94
+ projectId: PROJECT_ID,
95
+ rumApplicationId: RUM_APP_ID,
96
+ });
97
+
98
+ expect(daily.startsWith("replay:rate:bytes:")).toBe(true);
99
+ expect(monthly.startsWith("replay:rate:bytes-month:")).toBe(true);
100
+ expect(daily).not.toEqual(monthly);
101
+ });
102
+
103
+ test("keys are project-scoped", () => {
104
+ const otherProject: ObjectID = new ObjectID("60f7d9b0a1b2c3d4e5f60002");
105
+ expect(SessionReplayUsage.getDailyProjectByteKey(PROJECT_ID)).not.toEqual(
106
+ SessionReplayUsage.getDailyProjectByteKey(otherProject),
107
+ );
108
+ });
109
+ });
110
+
111
+ describe("getProjectBytesUsedToday", () => {
112
+ test("returns null (unknown) when there is no Redis client", async () => {
113
+ getClientMock.mockReturnValue(null);
114
+ isConnectedMock.mockReturnValue(false);
115
+
116
+ await expect(
117
+ SessionReplayUsage.getProjectBytesUsedToday(PROJECT_ID),
118
+ ).resolves.toBeNull();
119
+ });
120
+
121
+ test("returns null (unknown) when Redis is not connected", async () => {
122
+ getClientMock.mockReturnValue({ get: jest.fn() });
123
+ isConnectedMock.mockReturnValue(false);
124
+
125
+ await expect(
126
+ SessionReplayUsage.getProjectBytesUsedToday(PROJECT_ID),
127
+ ).resolves.toBeNull();
128
+ });
129
+
130
+ test("returns 0 when the counter key is absent", async () => {
131
+ const get: jest.Mock = jest.fn().mockResolvedValue(null);
132
+ mockConnectedClient(get);
133
+
134
+ await expect(
135
+ SessionReplayUsage.getProjectBytesUsedToday(PROJECT_ID),
136
+ ).resolves.toBe(0);
137
+
138
+ expect(get).toHaveBeenCalledWith(
139
+ SessionReplayUsage.getDailyProjectByteKey(PROJECT_ID),
140
+ );
141
+ });
142
+
143
+ test("parses the stored decimal string into a number", async () => {
144
+ const get: jest.Mock = jest.fn().mockResolvedValue("2048");
145
+ mockConnectedClient(get);
146
+
147
+ await expect(
148
+ SessionReplayUsage.getProjectBytesUsedToday(PROJECT_ID),
149
+ ).resolves.toBe(2048);
150
+ });
151
+
152
+ test("treats a non-numeric stored value as 0 rather than NaN", async () => {
153
+ const get: jest.Mock = jest.fn().mockResolvedValue("not-a-number");
154
+ mockConnectedClient(get);
155
+
156
+ await expect(
157
+ SessionReplayUsage.getProjectBytesUsedToday(PROJECT_ID),
158
+ ).resolves.toBe(0);
159
+ });
160
+
161
+ test("returns null (unknown) when the Redis read throws", async () => {
162
+ const get: jest.Mock = jest
163
+ .fn()
164
+ .mockRejectedValue(new Error("connection reset"));
165
+ mockConnectedClient(get);
166
+
167
+ await expect(
168
+ SessionReplayUsage.getProjectBytesUsedToday(PROJECT_ID),
169
+ ).resolves.toBeNull();
170
+ });
171
+ });
172
+
173
+ describe("getApplicationBytesUsedThisMonth", () => {
174
+ test("reads the monthly application key", async () => {
175
+ const get: jest.Mock = jest.fn().mockResolvedValue("500");
176
+ mockConnectedClient(get);
177
+
178
+ const used: number | null =
179
+ await SessionReplayUsage.getApplicationBytesUsedThisMonth({
180
+ projectId: PROJECT_ID,
181
+ rumApplicationId: RUM_APP_ID,
182
+ });
183
+
184
+ expect(used).toBe(500);
185
+ expect(get).toHaveBeenCalledWith(
186
+ SessionReplayUsage.getMonthlyApplicationByteKey({
187
+ projectId: PROJECT_ID,
188
+ rumApplicationId: RUM_APP_ID,
189
+ }),
190
+ );
191
+ });
192
+
193
+ test("returns null (unknown) when Redis is unavailable", async () => {
194
+ getClientMock.mockReturnValue(null);
195
+ isConnectedMock.mockReturnValue(false);
196
+
197
+ await expect(
198
+ SessionReplayUsage.getApplicationBytesUsedThisMonth({
199
+ projectId: PROJECT_ID,
200
+ rumApplicationId: RUM_APP_ID,
201
+ }),
202
+ ).resolves.toBeNull();
203
+ });
204
+ });
205
+ });