@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,407 @@
1
+ import UserSlackService from "../../../Server/Services/UserSlackService";
2
+ import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
3
+ import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
4
+ import WorkspaceUserAuthTokenService from "../../../Server/Services/WorkspaceUserAuthTokenService";
5
+ import SlackUtil from "../../../Server/Utils/Workspace/Slack/Slack";
6
+ import logger from "../../../Server/Utils/Logger";
7
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
8
+ import { OnCreate } from "../../../Server/Types/Database/Hooks";
9
+ import UserSlack from "../../../Models/DatabaseModels/UserSlack";
10
+ import WorkspaceProjectAuthToken from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
11
+ import WorkspaceUserAuthToken from "../../../Models/DatabaseModels/WorkspaceUserAuthToken";
12
+ import BadDataException from "../../../Types/Exception/BadDataException";
13
+ import ObjectID from "../../../Types/ObjectID";
14
+ import PositiveNumber from "../../../Types/PositiveNumber";
15
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
16
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
17
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
18
+
19
+ /*
20
+ * A UserSlack row is a POINTER at the user's existing OAuth workspace link,
21
+ * never a hand-typed address. Everything pinned here follows from that:
22
+ *
23
+ * 1. CREATION IS VERIFICATION. onBeforeCreate resolves the Slack member id
24
+ * from the caller's OWN WorkspaceUserAuthToken and stamps
25
+ * isVerified: true. A client-supplied slackUserId or isVerified is
26
+ * refused outright — accepting either would let a caller aim a bot at an
27
+ * arbitrary member id without ever proving they control that account.
28
+ *
29
+ * 2. THE GATES. No project connection, no user connection, or an existing
30
+ * row each refuse the create with a message that names the settings page
31
+ * that fixes it.
32
+ *
33
+ * 3. THE SEEDING. A row is born verified, so default on-call rules are
34
+ * seeded at create time (like webhooks) — and a seeding failure is
35
+ * logged, never thrown, because the method row itself is real.
36
+ *
37
+ * 4. THE CASCADE. Deleting the method deletes every rule pointing at it via
38
+ * the service hook, in addition to the FK cascade.
39
+ */
40
+
41
+ const PROJECT_ID: ObjectID = new ObjectID(
42
+ "11111111-1111-4111-8111-111111111111",
43
+ );
44
+ const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
45
+ const METHOD_ID: ObjectID = new ObjectID(
46
+ "33333333-3333-4333-8333-333333333333",
47
+ );
48
+
49
+ const SLACK_MEMBER_ID: string = "U0123ABCD";
50
+ const BOT_TOKEN: string = "xoxb-project-bot-token";
51
+
52
+ type OnBeforeCreateFunction = (
53
+ createBy: CreateBy<UserSlack>,
54
+ ) => Promise<OnCreate<UserSlack>>;
55
+
56
+ type OnCreateSuccessFunction = (
57
+ onCreate: OnCreate<UserSlack>,
58
+ createdItem: UserSlack,
59
+ ) => Promise<UserSlack>;
60
+
61
+ type OnBeforeDeleteFunction = (deleteBy: {
62
+ query: Record<string, unknown>;
63
+ props: { isRoot: boolean };
64
+ }) => Promise<unknown>;
65
+
66
+ function callOnBeforeCreate(
67
+ createBy: CreateBy<UserSlack>,
68
+ ): Promise<OnCreate<UserSlack>> {
69
+ return (
70
+ UserSlackService as unknown as { onBeforeCreate: OnBeforeCreateFunction }
71
+ ).onBeforeCreate(createBy);
72
+ }
73
+
74
+ function callOnCreateSuccess(
75
+ onCreate: OnCreate<UserSlack>,
76
+ createdItem: UserSlack,
77
+ ): Promise<UserSlack> {
78
+ return (
79
+ UserSlackService as unknown as {
80
+ onCreateSuccess: OnCreateSuccessFunction;
81
+ }
82
+ ).onCreateSuccess(onCreate, createdItem);
83
+ }
84
+
85
+ function callOnBeforeDelete(deleteBy: {
86
+ query: Record<string, unknown>;
87
+ props: { isRoot: boolean };
88
+ }): Promise<unknown> {
89
+ return (
90
+ UserSlackService as unknown as { onBeforeDelete: OnBeforeDeleteFunction }
91
+ ).onBeforeDelete(deleteBy);
92
+ }
93
+
94
+ function createBy(
95
+ data: Partial<UserSlack> = {},
96
+ props: { isRoot: boolean } = { isRoot: false },
97
+ ): CreateBy<UserSlack> {
98
+ return {
99
+ data: {
100
+ projectId: PROJECT_ID,
101
+ userId: USER_ID,
102
+ ...data,
103
+ } as UserSlack,
104
+ props: props,
105
+ } as CreateBy<UserSlack>;
106
+ }
107
+
108
+ describe("UserSlackService", () => {
109
+ let countBy: jest.SpyInstance;
110
+ let getProjectAuth: jest.SpyInstance;
111
+ let getUserAuth: jest.SpyInstance;
112
+ let getUsername: jest.SpyInstance;
113
+ let loggerError: jest.SpyInstance;
114
+
115
+ beforeEach(() => {
116
+ loggerError = jest.spyOn(logger, "error").mockImplementation((): void => {
117
+ return undefined;
118
+ });
119
+
120
+ countBy = jest
121
+ .spyOn(UserSlackService, "countBy")
122
+ .mockResolvedValue(new PositiveNumber(0) as never);
123
+
124
+ getProjectAuth = jest
125
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
126
+ .mockResolvedValue({
127
+ id: PROJECT_ID,
128
+ authToken: BOT_TOKEN,
129
+ } as unknown as WorkspaceProjectAuthToken as never);
130
+
131
+ getUserAuth = jest
132
+ .spyOn(WorkspaceUserAuthTokenService, "getUserAuth")
133
+ .mockResolvedValue({
134
+ workspaceUserId: SLACK_MEMBER_ID,
135
+ } as unknown as WorkspaceUserAuthToken as never);
136
+
137
+ getUsername = jest
138
+ .spyOn(SlackUtil, "getUsernameFromUserId")
139
+ .mockResolvedValue("alice" as never);
140
+ });
141
+
142
+ afterEach(() => {
143
+ jest.restoreAllMocks();
144
+ });
145
+
146
+ /*
147
+ * ----------------------------------------------------------------------- *
148
+ * (A) onBeforeCreate — creation is verification.
149
+ * -----------------------------------------------------------------------
150
+ */
151
+
152
+ describe("onBeforeCreate", () => {
153
+ test("resolves the member id from the user's own workspace link and stamps verified", async () => {
154
+ const result: OnCreate<UserSlack> = await callOnBeforeCreate(createBy());
155
+
156
+ expect(result.createBy.data.slackUserId).toBe(SLACK_MEMBER_ID);
157
+ expect(result.createBy.data.isVerified).toBe(true);
158
+ expect(result.createBy.data.slackUserName).toBe("alice");
159
+
160
+ // The link is resolved for exactly this (project, user, Slack).
161
+ const authArg: {
162
+ projectId: ObjectID;
163
+ userId: ObjectID;
164
+ workspaceType: WorkspaceType;
165
+ } = getUserAuth.mock.calls[0][0] as {
166
+ projectId: ObjectID;
167
+ userId: ObjectID;
168
+ workspaceType: WorkspaceType;
169
+ };
170
+ expect(authArg.projectId.toString()).toBe(PROJECT_ID.toString());
171
+ expect(authArg.userId.toString()).toBe(USER_ID.toString());
172
+ expect(authArg.workspaceType).toBe(WorkspaceType.Slack);
173
+ });
174
+
175
+ test("a non-root caller supplying isVerified is refused", async () => {
176
+ await expect(
177
+ callOnBeforeCreate(createBy({ isVerified: true })),
178
+ ).rejects.toThrow("isVerified cannot be set to true");
179
+ });
180
+
181
+ test("a non-root caller supplying slackUserId is refused - the address is never client input", async () => {
182
+ await expect(
183
+ callOnBeforeCreate(createBy({ slackUserId: "U-ATTACKER" })),
184
+ ).rejects.toThrow("slackUserId cannot be set directly");
185
+ });
186
+
187
+ test("a project that is not connected to Slack is refused with the settings pointer", async () => {
188
+ getProjectAuth.mockResolvedValue(null as never);
189
+
190
+ await expect(callOnBeforeCreate(createBy())).rejects.toThrow(
191
+ "This project is not connected to Slack. Please ask a project admin to connect Slack in Project Settings > Slack Integration.",
192
+ );
193
+ });
194
+
195
+ test("a user who has not connected their Slack account is refused with the settings pointer", async () => {
196
+ getUserAuth.mockResolvedValue(null as never);
197
+
198
+ await expect(callOnBeforeCreate(createBy())).rejects.toThrow(
199
+ "Your Slack account is not connected to OneUptime for this project. Please go to User Settings > Slack Integration and connect your Slack account first.",
200
+ );
201
+ });
202
+
203
+ test("a user auth row with no workspaceUserId counts as not connected", async () => {
204
+ getUserAuth.mockResolvedValue({
205
+ workspaceUserId: undefined,
206
+ } as unknown as WorkspaceUserAuthToken as never);
207
+
208
+ await expect(callOnBeforeCreate(createBy())).rejects.toThrow(
209
+ BadDataException,
210
+ );
211
+ });
212
+
213
+ test("a second row for the same (user, project) is refused as a duplicate", async () => {
214
+ countBy.mockResolvedValue(new PositiveNumber(1) as never);
215
+
216
+ await expect(callOnBeforeCreate(createBy())).rejects.toThrow(
217
+ "Slack is already added as a notification method for this project.",
218
+ );
219
+
220
+ // Refused before any workspace lookup happens.
221
+ expect(getUserAuth).not.toHaveBeenCalled();
222
+ });
223
+
224
+ test("a missing projectId or userId is refused before anything is queried", async () => {
225
+ await expect(
226
+ callOnBeforeCreate({
227
+ data: { projectId: PROJECT_ID } as UserSlack,
228
+ props: { isRoot: false },
229
+ } as CreateBy<UserSlack>),
230
+ ).rejects.toThrow("projectId and userId are required");
231
+
232
+ expect(countBy).not.toHaveBeenCalled();
233
+ });
234
+
235
+ test("a username lookup failure is logged and does NOT stop the method being added", async () => {
236
+ getUsername.mockRejectedValue(new Error("slack api down") as never);
237
+
238
+ const result: OnCreate<UserSlack> = await callOnBeforeCreate(createBy());
239
+
240
+ expect(result.createBy.data.slackUserId).toBe(SLACK_MEMBER_ID);
241
+ expect(result.createBy.data.isVerified).toBe(true);
242
+ expect(result.createBy.data.slackUserName).toBeUndefined();
243
+ expect(loggerError).toHaveBeenCalled();
244
+ });
245
+
246
+ test("the username lookup uses the PROJECT bot token, not the user's token", async () => {
247
+ await callOnBeforeCreate(createBy());
248
+
249
+ const arg: { authToken: string; userId: string } = getUsername.mock
250
+ .calls[0][0] as { authToken: string; userId: string };
251
+ expect(arg.authToken).toBe(BOT_TOKEN);
252
+ expect(arg.userId).toBe(SLACK_MEMBER_ID);
253
+ });
254
+ });
255
+
256
+ /*
257
+ * ----------------------------------------------------------------------- *
258
+ * (B) onCreateSuccess — default-rule seeding.
259
+ * -----------------------------------------------------------------------
260
+ */
261
+
262
+ describe("onCreateSuccess", () => {
263
+ let seedRules: jest.SpyInstance;
264
+
265
+ beforeEach(() => {
266
+ seedRules = jest
267
+ .spyOn(
268
+ UserNotificationRuleService,
269
+ "addDefaultNotificationRulesForVerifiedMethod",
270
+ )
271
+ .mockResolvedValue(undefined as never);
272
+ });
273
+
274
+ test("seeds default rules pointed at the new row via userSlackId", async () => {
275
+ const created: UserSlack = {
276
+ id: METHOD_ID,
277
+ projectId: PROJECT_ID,
278
+ userId: USER_ID,
279
+ } as unknown as UserSlack;
280
+
281
+ await callOnCreateSuccess(
282
+ { createBy: createBy(), carryForward: null } as OnCreate<UserSlack>,
283
+ created,
284
+ );
285
+
286
+ expect(seedRules).toHaveBeenCalledTimes(1);
287
+ const arg: {
288
+ projectId: ObjectID;
289
+ userId: ObjectID;
290
+ notificationMethod: { userSlackId: ObjectID };
291
+ } = seedRules.mock.calls[0][0] as {
292
+ projectId: ObjectID;
293
+ userId: ObjectID;
294
+ notificationMethod: { userSlackId: ObjectID };
295
+ };
296
+ expect(arg.projectId.toString()).toBe(PROJECT_ID.toString());
297
+ expect(arg.userId.toString()).toBe(USER_ID.toString());
298
+ expect(arg.notificationMethod.userSlackId.toString()).toBe(
299
+ METHOD_ID.toString(),
300
+ );
301
+ });
302
+
303
+ test("a seeding failure is swallowed and logged - the method row is already real", async () => {
304
+ seedRules.mockRejectedValue(new Error("severity read failed") as never);
305
+
306
+ const created: UserSlack = {
307
+ id: METHOD_ID,
308
+ projectId: PROJECT_ID,
309
+ userId: USER_ID,
310
+ } as unknown as UserSlack;
311
+
312
+ await expect(
313
+ callOnCreateSuccess(
314
+ { createBy: createBy(), carryForward: null } as OnCreate<UserSlack>,
315
+ created,
316
+ ),
317
+ ).resolves.toBe(created);
318
+
319
+ expect(loggerError).toHaveBeenCalled();
320
+ });
321
+
322
+ test("an item missing its ids seeds nothing rather than seeding garbage", async () => {
323
+ await callOnCreateSuccess(
324
+ { createBy: createBy(), carryForward: null } as OnCreate<UserSlack>,
325
+ { id: METHOD_ID } as unknown as UserSlack,
326
+ );
327
+
328
+ expect(seedRules).not.toHaveBeenCalled();
329
+ });
330
+ });
331
+
332
+ /*
333
+ * ----------------------------------------------------------------------- *
334
+ * (C) onBeforeDelete — the rule cascade.
335
+ * -----------------------------------------------------------------------
336
+ */
337
+
338
+ describe("onBeforeDelete", () => {
339
+ test("deletes every notification rule pointing at each row being deleted", async () => {
340
+ jest.spyOn(UserSlackService, "findBy").mockResolvedValue([
341
+ {
342
+ id: METHOD_ID,
343
+ projectId: PROJECT_ID,
344
+ } as unknown as UserSlack,
345
+ ] as never);
346
+
347
+ const deleteRules: jest.SpyInstance = jest
348
+ .spyOn(UserNotificationRuleService, "deleteBy")
349
+ .mockResolvedValue([] as never);
350
+
351
+ await callOnBeforeDelete({
352
+ query: { _id: METHOD_ID },
353
+ props: { isRoot: true },
354
+ });
355
+
356
+ expect(deleteRules).toHaveBeenCalledTimes(1);
357
+ const arg: {
358
+ query: { userSlackId: ObjectID; projectId: ObjectID };
359
+ limit: number;
360
+ props: { isRoot: boolean };
361
+ } = deleteRules.mock.calls[0][0] as {
362
+ query: { userSlackId: ObjectID; projectId: ObjectID };
363
+ limit: number;
364
+ props: { isRoot: boolean };
365
+ };
366
+ expect(arg.query.userSlackId.toString()).toBe(METHOD_ID.toString());
367
+ expect(arg.query.projectId.toString()).toBe(PROJECT_ID.toString());
368
+ expect(arg.limit).toBe(LIMIT_MAX);
369
+ expect(arg.props.isRoot).toBe(true);
370
+ });
371
+ });
372
+
373
+ /*
374
+ * ----------------------------------------------------------------------- *
375
+ * (D) getDeletionImpact — the shared preview, on the Slack channel.
376
+ * -----------------------------------------------------------------------
377
+ */
378
+
379
+ describe("getDeletionImpact", () => {
380
+ test("delegates to the shared preview with methodType Slack", async () => {
381
+ const getImpact: jest.SpyInstance = jest
382
+ .spyOn(
383
+ UserNotificationRuleService,
384
+ "getNotificationMethodDeletionImpact",
385
+ )
386
+ .mockResolvedValue({} as never);
387
+
388
+ await UserSlackService.getDeletionImpact({
389
+ itemId: METHOD_ID,
390
+ projectId: PROJECT_ID,
391
+ });
392
+
393
+ const arg: {
394
+ projectId: ObjectID;
395
+ methodType: string;
396
+ methodId: ObjectID;
397
+ } = getImpact.mock.calls[0][0] as {
398
+ projectId: ObjectID;
399
+ methodType: string;
400
+ methodId: ObjectID;
401
+ };
402
+ expect(arg.projectId.toString()).toBe(PROJECT_ID.toString());
403
+ expect(arg.methodType).toBe("Slack");
404
+ expect(arg.methodId.toString()).toBe(METHOD_ID.toString());
405
+ });
406
+ });
407
+ });
@@ -0,0 +1,224 @@
1
+ import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
2
+ import WorkspaceUserAuthTokenService from "../../../Server/Services/WorkspaceUserAuthTokenService";
3
+ import WorkspaceProjectAuthToken from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
4
+ import ObjectID from "../../../Types/ObjectID";
5
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
6
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
7
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
8
+
9
+ /*
10
+ * The PROJECT-level half of the workspace disconnect cascade.
11
+ *
12
+ * The dashboard's "Uninstall OneUptime from Slack / Microsoft Teams" button
13
+ * deletes the WorkspaceProjectAuthToken row through the generic CRUD path.
14
+ * Before this hook existed, that path deleted ONLY the project token: every
15
+ * member's WorkspaceUserAuthToken survived, and with it every UserSlack /
16
+ * UserMicrosoftTeams notification method — verified-looking rows pointing at
17
+ * a workspace the sender deterministically refuses. The on-call fallback
18
+ * would pick them into its zero-cost tier and stop looking, and the
19
+ * readiness surface would keep reporting the responder reachable: the exact
20
+ * false green both of those surfaces exist to make impossible. (The
21
+ * Slack-side app_uninstall webhook already deleted the user tokens — but it
22
+ * only fires for uninstalls initiated inside Slack, and Microsoft Teams has
23
+ * no uninstall webhook at all.)
24
+ *
25
+ * The hook deletes the workspace's user tokens THROUGH THEIR SERVICE, so
26
+ * WorkspaceUserAuthTokenService.onBeforeDelete fires and cascades to the
27
+ * notification methods and their rules — one chain, tested per link:
28
+ * project token -> user tokens (here), user token -> method rows
29
+ * (WorkspaceUserAuthTokenNotificationMethodCascade.test.ts), method row ->
30
+ * rules (UserSlackService.test.ts / UserMicrosoftTeamsService.test.ts).
31
+ */
32
+
33
+ const PROJECT_ID: ObjectID = new ObjectID(
34
+ "11111111-1111-4111-8111-111111111111",
35
+ );
36
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
37
+ "22222222-2222-4222-8222-222222222222",
38
+ );
39
+ const TOKEN_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
40
+
41
+ type OnBeforeDeleteFunction = (deleteBy: {
42
+ query: Record<string, unknown>;
43
+ props: { isRoot: boolean };
44
+ }) => Promise<unknown>;
45
+
46
+ function callOnBeforeDelete(deleteBy: {
47
+ query: Record<string, unknown>;
48
+ props: { isRoot: boolean };
49
+ }): Promise<unknown> {
50
+ return (
51
+ WorkspaceProjectAuthTokenService as unknown as {
52
+ onBeforeDelete: OnBeforeDeleteFunction;
53
+ }
54
+ ).onBeforeDelete(deleteBy);
55
+ }
56
+
57
+ function projectTokenRow(
58
+ workspaceType: WorkspaceType,
59
+ projectId: ObjectID = PROJECT_ID,
60
+ ): WorkspaceProjectAuthToken {
61
+ return {
62
+ id: TOKEN_ID,
63
+ projectId: projectId,
64
+ workspaceType: workspaceType,
65
+ } as unknown as WorkspaceProjectAuthToken;
66
+ }
67
+
68
+ describe("WorkspaceProjectAuthTokenService.onBeforeDelete - workspace disconnect cascade", () => {
69
+ let findTokens: jest.SpyInstance;
70
+ let deleteUserTokens: jest.SpyInstance;
71
+
72
+ beforeEach(() => {
73
+ findTokens = jest
74
+ .spyOn(WorkspaceProjectAuthTokenService, "findBy")
75
+ .mockResolvedValue([] as never);
76
+
77
+ deleteUserTokens = jest
78
+ .spyOn(WorkspaceUserAuthTokenService, "deleteBy")
79
+ .mockResolvedValue([] as never);
80
+ });
81
+
82
+ afterEach(() => {
83
+ jest.restoreAllMocks();
84
+ });
85
+
86
+ test("deleting the Slack project token deletes every Slack user token in the project, through the service", async () => {
87
+ findTokens.mockResolvedValue([
88
+ projectTokenRow(WorkspaceType.Slack),
89
+ ] as never);
90
+
91
+ await callOnBeforeDelete({
92
+ query: { _id: TOKEN_ID },
93
+ props: { isRoot: false },
94
+ });
95
+
96
+ expect(deleteUserTokens).toHaveBeenCalledTimes(1);
97
+ const arg: {
98
+ query: { projectId: ObjectID; workspaceType: WorkspaceType };
99
+ limit: number;
100
+ props: { isRoot: boolean };
101
+ } = deleteUserTokens.mock.calls[0][0] as {
102
+ query: { projectId: ObjectID; workspaceType: WorkspaceType };
103
+ limit: number;
104
+ props: { isRoot: boolean };
105
+ };
106
+ expect(arg.query.projectId.toString()).toBe(PROJECT_ID.toString());
107
+ expect(arg.query.workspaceType).toBe(WorkspaceType.Slack);
108
+ expect(arg.limit).toBe(LIMIT_MAX);
109
+ /*
110
+ * isRoot matters twice over: the user tokens belong to OTHER users, whom
111
+ * the disconnecting admin's own scope could never delete — and it is the
112
+ * deleteBy going THROUGH the service (rather than raw) that fires the
113
+ * user-token hook which takes the notification methods down with it.
114
+ */
115
+ expect(arg.props.isRoot).toBe(true);
116
+ });
117
+
118
+ test("the cascade is scoped to the WORKSPACE TYPE being disconnected - a Teams disconnect leaves Slack links alone", async () => {
119
+ findTokens.mockResolvedValue([
120
+ projectTokenRow(WorkspaceType.MicrosoftTeams),
121
+ ] as never);
122
+
123
+ await callOnBeforeDelete({
124
+ query: { _id: TOKEN_ID },
125
+ props: { isRoot: true },
126
+ });
127
+
128
+ expect(deleteUserTokens).toHaveBeenCalledTimes(1);
129
+ const arg: { query: { workspaceType: WorkspaceType } } = deleteUserTokens
130
+ .mock.calls[0][0] as { query: { workspaceType: WorkspaceType } };
131
+ expect(arg.query.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
132
+ });
133
+
134
+ test("a bulk delete spanning both workspace types cascades each token to its own workspace", async () => {
135
+ findTokens.mockResolvedValue([
136
+ projectTokenRow(WorkspaceType.Slack),
137
+ projectTokenRow(WorkspaceType.MicrosoftTeams),
138
+ ] as never);
139
+
140
+ await callOnBeforeDelete({
141
+ query: { projectId: PROJECT_ID },
142
+ props: { isRoot: true },
143
+ });
144
+
145
+ expect(deleteUserTokens).toHaveBeenCalledTimes(2);
146
+ const workspaceTypes: Array<WorkspaceType> =
147
+ deleteUserTokens.mock.calls.map((call: Array<unknown>) => {
148
+ return (call[0] as { query: { workspaceType: WorkspaceType } }).query
149
+ .workspaceType;
150
+ });
151
+ expect(workspaceTypes).toContain(WorkspaceType.Slack);
152
+ expect(workspaceTypes).toContain(WorkspaceType.MicrosoftTeams);
153
+ });
154
+
155
+ test("each token's cascade is scoped to that token's OWN project", async () => {
156
+ findTokens.mockResolvedValue([
157
+ projectTokenRow(WorkspaceType.Slack, PROJECT_ID),
158
+ projectTokenRow(WorkspaceType.Slack, OTHER_PROJECT_ID),
159
+ ] as never);
160
+
161
+ await callOnBeforeDelete({
162
+ query: { workspaceType: WorkspaceType.Slack },
163
+ props: { isRoot: true },
164
+ });
165
+
166
+ const projectIds: Array<string> = deleteUserTokens.mock.calls.map(
167
+ (call: Array<unknown>) => {
168
+ return (
169
+ call[0] as { query: { projectId: ObjectID } }
170
+ ).query.projectId.toString();
171
+ },
172
+ );
173
+ expect(projectIds).toContain(PROJECT_ID.toString());
174
+ expect(projectIds).toContain(OTHER_PROJECT_ID.toString());
175
+ });
176
+
177
+ test("the tokens are re-read with the RAW delete query, as root", async () => {
178
+ await callOnBeforeDelete({
179
+ query: { projectId: PROJECT_ID },
180
+ props: { isRoot: false },
181
+ });
182
+
183
+ expect(findTokens).toHaveBeenCalledTimes(1);
184
+ const arg: {
185
+ query: Record<string, unknown>;
186
+ props: { isRoot: boolean };
187
+ limit: number;
188
+ } = findTokens.mock.calls[0][0] as {
189
+ query: Record<string, unknown>;
190
+ props: { isRoot: boolean };
191
+ limit: number;
192
+ };
193
+ expect(arg.query["projectId"]).toBe(PROJECT_ID);
194
+ expect(arg.props.isRoot).toBe(true);
195
+ expect(arg.limit).toBe(LIMIT_MAX);
196
+ });
197
+
198
+ test("a token row missing its project or workspace type is skipped rather than issuing an unscoped delete", async () => {
199
+ findTokens.mockResolvedValue([
200
+ { id: TOKEN_ID } as unknown as WorkspaceProjectAuthToken,
201
+ ] as never);
202
+
203
+ await callOnBeforeDelete({
204
+ query: { _id: TOKEN_ID },
205
+ props: { isRoot: true },
206
+ });
207
+
208
+ /*
209
+ * A deleteBy whose query is { projectId: undefined, workspaceType:
210
+ * undefined } would match every user token in the installation. Skipping
211
+ * the malformed row is the only safe reading.
212
+ */
213
+ expect(deleteUserTokens).not.toHaveBeenCalled();
214
+ });
215
+
216
+ test("a delete that matches no project tokens cascades nothing", async () => {
217
+ await callOnBeforeDelete({
218
+ query: { _id: TOKEN_ID },
219
+ props: { isRoot: true },
220
+ });
221
+
222
+ expect(deleteUserTokens).not.toHaveBeenCalled();
223
+ });
224
+ });