@oneuptime/common 12.0.18 → 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 (206) 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/EnterpriseLicenseService.ts +60 -0
  20. package/Server/Services/Index.ts +6 -2
  21. package/Server/Services/LlmLogService.ts +25 -9
  22. package/Server/Services/OnCallReadinessService.ts +86 -2
  23. package/Server/Services/ProjectService.ts +67 -0
  24. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  25. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  26. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  27. package/Server/Services/UserNotificationRuleService.ts +589 -11
  28. package/Server/Services/UserNotificationSettingService.ts +138 -0
  29. package/Server/Services/UserService.ts +28 -0
  30. package/Server/Services/UserSlackService.ts +218 -0
  31. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  32. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  33. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  34. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  35. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  36. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  37. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  38. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  39. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  40. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  41. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  42. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  43. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  44. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  45. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  46. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  47. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  48. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  49. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  50. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  51. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  52. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  53. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  54. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  55. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  56. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  57. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  58. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  59. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  60. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  61. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  62. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  63. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  64. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  65. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  66. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  67. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  68. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  69. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  70. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  71. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  72. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  73. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  74. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  75. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  76. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  77. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  78. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  79. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  80. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  81. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  82. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  83. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  84. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  85. package/Types/Marketing/MarketingEvent.ts +97 -0
  86. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  87. package/UI/Utils/PermissionGate.ts +56 -0
  88. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  89. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  91. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  93. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  95. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  96. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  97. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  99. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  101. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  103. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  104. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  105. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  106. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  107. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  108. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  109. package/build/dist/Server/EnvironmentConfig.js +10 -70
  110. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  121. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  122. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  123. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  124. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  125. package/build/dist/Server/Services/Index.js +6 -2
  126. package/build/dist/Server/Services/Index.js.map +1 -1
  127. package/build/dist/Server/Services/LlmLogService.js +25 -9
  128. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  129. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  130. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  131. package/build/dist/Server/Services/ProjectService.js +56 -0
  132. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  133. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  134. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  135. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  136. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  138. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  139. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  140. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  141. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  142. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  143. package/build/dist/Server/Services/UserService.js +26 -1
  144. package/build/dist/Server/Services/UserService.js.map +1 -1
  145. package/build/dist/Server/Services/UserSlackService.js +195 -0
  146. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  147. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  148. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  149. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  150. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  151. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  152. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  153. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  154. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  155. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  156. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  157. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  158. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  160. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  163. package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
  164. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  165. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  166. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  167. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  168. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  169. package/package.json +1 -1
  170. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  171. package/Server/Services/MarketingConversionService.ts +0 -10
  172. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  173. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  174. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  175. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  176. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  177. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  178. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  179. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  180. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  181. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  182. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  183. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  184. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  185. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  186. package/Types/Marketing/MarketingConversion.ts +0 -53
  187. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  188. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  189. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  190. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  191. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  192. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  193. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  194. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  195. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  196. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  197. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  198. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  199. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  200. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  201. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  202. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  203. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  204. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  205. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  206. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,189 @@
1
+ import WorkspaceUserAuthTokenService from "../../../Server/Services/WorkspaceUserAuthTokenService";
2
+ import UserSlackService from "../../../Server/Services/UserSlackService";
3
+ import UserMicrosoftTeamsService from "../../../Server/Services/UserMicrosoftTeamsService";
4
+ import WorkspaceUserAuthToken from "../../../Models/DatabaseModels/WorkspaceUserAuthToken";
5
+ import ObjectID from "../../../Types/ObjectID";
6
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
7
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
8
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
9
+
10
+ /*
11
+ * Disconnecting a workspace account takes the notification methods pointing at
12
+ * it down too — deliberately. A UserSlack / UserMicrosoftTeams row is a
13
+ * pointer at the WorkspaceUserAuthToken being deleted here, and the two
14
+ * possible stale states are not equivalent:
15
+ *
16
+ * - Method row kept: rules keep selecting it, every page becomes an Error
17
+ * timeline row, and nothing re-pages the responder. Silent.
18
+ * - Method row deleted: its rules cascade away with it (the method service's
19
+ * own delete hook), the responder's cells go back to "no rule", and the
20
+ * verified-method fallback rescues the next page.
21
+ *
22
+ * The second is the designed failure mode, so the cascade below is what keeps
23
+ * a disconnect from becoming a silent unpageable responder. Pinned per
24
+ * workspace type, because deleting a Slack link must not touch the Teams
25
+ * method and vice versa.
26
+ */
27
+
28
+ const PROJECT_ID: ObjectID = new ObjectID(
29
+ "11111111-1111-4111-8111-111111111111",
30
+ );
31
+ const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
32
+ const TOKEN_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
33
+
34
+ type OnBeforeDeleteFunction = (deleteBy: {
35
+ query: Record<string, unknown>;
36
+ props: { isRoot: boolean };
37
+ }) => Promise<unknown>;
38
+
39
+ function callOnBeforeDelete(deleteBy: {
40
+ query: Record<string, unknown>;
41
+ props: { isRoot: boolean };
42
+ }): Promise<unknown> {
43
+ return (
44
+ WorkspaceUserAuthTokenService as unknown as {
45
+ onBeforeDelete: OnBeforeDeleteFunction;
46
+ }
47
+ ).onBeforeDelete(deleteBy);
48
+ }
49
+
50
+ function tokenRow(workspaceType: WorkspaceType): WorkspaceUserAuthToken {
51
+ return {
52
+ id: TOKEN_ID,
53
+ projectId: PROJECT_ID,
54
+ userId: USER_ID,
55
+ workspaceType: workspaceType,
56
+ } as unknown as WorkspaceUserAuthToken;
57
+ }
58
+
59
+ describe("WorkspaceUserAuthTokenService.onBeforeDelete - notification method cascade", () => {
60
+ let findTokens: jest.SpyInstance;
61
+ let deleteSlack: jest.SpyInstance;
62
+ let deleteTeams: jest.SpyInstance;
63
+
64
+ beforeEach(() => {
65
+ findTokens = jest
66
+ .spyOn(WorkspaceUserAuthTokenService, "findBy")
67
+ .mockResolvedValue([] as never);
68
+
69
+ deleteSlack = jest
70
+ .spyOn(UserSlackService, "deleteBy")
71
+ .mockResolvedValue([] as never);
72
+
73
+ deleteTeams = jest
74
+ .spyOn(UserMicrosoftTeamsService, "deleteBy")
75
+ .mockResolvedValue([] as never);
76
+ });
77
+
78
+ afterEach(() => {
79
+ jest.restoreAllMocks();
80
+ });
81
+
82
+ test("deleting a Slack link deletes the user's UserSlack rows for that project - and nothing else", async () => {
83
+ findTokens.mockResolvedValue([tokenRow(WorkspaceType.Slack)] as never);
84
+
85
+ await callOnBeforeDelete({
86
+ query: { _id: TOKEN_ID },
87
+ props: { isRoot: true },
88
+ });
89
+
90
+ expect(deleteSlack).toHaveBeenCalledTimes(1);
91
+ const arg: {
92
+ query: { projectId: ObjectID; userId: ObjectID };
93
+ limit: number;
94
+ props: { isRoot: boolean };
95
+ } = deleteSlack.mock.calls[0][0] as {
96
+ query: { projectId: ObjectID; userId: ObjectID };
97
+ limit: number;
98
+ props: { isRoot: boolean };
99
+ };
100
+ expect(arg.query.projectId.toString()).toBe(PROJECT_ID.toString());
101
+ expect(arg.query.userId.toString()).toBe(USER_ID.toString());
102
+ expect(arg.limit).toBe(LIMIT_MAX);
103
+ expect(arg.props.isRoot).toBe(true);
104
+
105
+ expect(deleteTeams).not.toHaveBeenCalled();
106
+ });
107
+
108
+ test("deleting a Microsoft Teams link deletes the user's UserMicrosoftTeams rows - and nothing else", async () => {
109
+ findTokens.mockResolvedValue([
110
+ tokenRow(WorkspaceType.MicrosoftTeams),
111
+ ] as never);
112
+
113
+ await callOnBeforeDelete({
114
+ query: { _id: TOKEN_ID },
115
+ props: { isRoot: true },
116
+ });
117
+
118
+ expect(deleteTeams).toHaveBeenCalledTimes(1);
119
+ expect(deleteSlack).not.toHaveBeenCalled();
120
+ });
121
+
122
+ test("a bulk delete spanning both workspace types cascades each row to its own channel", async () => {
123
+ findTokens.mockResolvedValue([
124
+ tokenRow(WorkspaceType.Slack),
125
+ tokenRow(WorkspaceType.MicrosoftTeams),
126
+ ] as never);
127
+
128
+ await callOnBeforeDelete({
129
+ query: { projectId: PROJECT_ID },
130
+ props: { isRoot: true },
131
+ });
132
+
133
+ expect(deleteSlack).toHaveBeenCalledTimes(1);
134
+ expect(deleteTeams).toHaveBeenCalledTimes(1);
135
+ });
136
+
137
+ test("the tokens are re-read with the RAW delete query, as root, before the delete narrows anything", async () => {
138
+ await callOnBeforeDelete({
139
+ query: { userId: USER_ID, workspaceType: WorkspaceType.Slack },
140
+ props: { isRoot: false },
141
+ });
142
+
143
+ expect(findTokens).toHaveBeenCalledTimes(1);
144
+ const arg: {
145
+ query: Record<string, unknown>;
146
+ props: { isRoot: boolean };
147
+ limit: number;
148
+ } = findTokens.mock.calls[0][0] as {
149
+ query: Record<string, unknown>;
150
+ props: { isRoot: boolean };
151
+ limit: number;
152
+ };
153
+ expect(arg.query["userId"]).toBe(USER_ID);
154
+ expect(arg.props.isRoot).toBe(true);
155
+ expect(arg.limit).toBe(LIMIT_MAX);
156
+ });
157
+
158
+ test("a token row missing its project or user ids is skipped rather than issuing an unscoped delete", async () => {
159
+ findTokens.mockResolvedValue([
160
+ {
161
+ id: TOKEN_ID,
162
+ workspaceType: WorkspaceType.Slack,
163
+ } as unknown as WorkspaceUserAuthToken,
164
+ ] as never);
165
+
166
+ await callOnBeforeDelete({
167
+ query: { _id: TOKEN_ID },
168
+ props: { isRoot: true },
169
+ });
170
+
171
+ /*
172
+ * The dangerous alternative: a deleteBy whose query is
173
+ * { projectId: undefined, userId: undefined } matches every row in the
174
+ * table. Skipping the malformed row is the only safe reading.
175
+ */
176
+ expect(deleteSlack).not.toHaveBeenCalled();
177
+ expect(deleteTeams).not.toHaveBeenCalled();
178
+ });
179
+
180
+ test("a delete that matches no tokens cascades nothing", async () => {
181
+ await callOnBeforeDelete({
182
+ query: { _id: TOKEN_ID },
183
+ props: { isRoot: true },
184
+ });
185
+
186
+ expect(deleteSlack).not.toHaveBeenCalled();
187
+ expect(deleteTeams).not.toHaveBeenCalled();
188
+ });
189
+ });
@@ -0,0 +1,496 @@
1
+ import WorkspaceUserNotificationService from "../../../Server/Services/WorkspaceUserNotificationService";
2
+ import WorkspaceProjectAuthTokenService from "../../../Server/Services/WorkspaceProjectAuthTokenService";
3
+ import WorkspaceNotificationLogService from "../../../Server/Services/WorkspaceNotificationLogService";
4
+ import UserOnCallLogTimelineService from "../../../Server/Services/UserOnCallLogTimelineService";
5
+ import SlackUtil from "../../../Server/Utils/Workspace/Slack/Slack";
6
+ import MicrosoftTeamsUtil from "../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
7
+ import logger from "../../../Server/Utils/Logger";
8
+ import WorkspaceProjectAuthToken from "../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
9
+ import BadDataException from "../../../Types/Exception/BadDataException";
10
+ import ObjectID from "../../../Types/ObjectID";
11
+ import UserNotificationStatus from "../../../Types/UserNotification/UserNotificationStatus";
12
+ import WorkspaceNotificationActionType from "../../../Types/Workspace/WorkspaceNotificationActionType";
13
+ import WorkspaceNotificationStatus from "../../../Types/Workspace/WorkspaceNotificationStatus";
14
+ import {
15
+ WorkspaceMessageBlock,
16
+ WorkspacePayloadMarkdown,
17
+ } from "../../../Types/Workspace/WorkspaceMessagePayload";
18
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
19
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
20
+
21
+ /*
22
+ * WorkspaceUserNotificationService.sendDirectMessageToUser is the send half of
23
+ * the Slack / Microsoft Teams notification methods: everything the on-call
24
+ * pipeline and the settings-based notifications know about delivering a
25
+ * workspace DM funnels through this one method. Four things about it are
26
+ * load-bearing and pinned here:
27
+ *
28
+ * 1. RESOLUTION. The PROJECT's bot credentials do the sending — never the
29
+ * user's own token — and a project that is not connected fails with the
30
+ * exact actionable message the timeline will carry.
31
+ *
32
+ * 2. DISPATCH. Slack sends through SlackUtil with the bot token; Teams
33
+ * sends through the Bot Framework helper with the projectId (Teams
34
+ * resolves its own credentials). Crossing those up would page nobody.
35
+ *
36
+ * 3. THE TIMELINE CONTRACT. When a userOnCallLogTimelineId is supplied the
37
+ * row is flipped to Sent on success and Error (with the send error's own
38
+ * message) on failure — mirroring what the Notification FeatureSet does
39
+ * for SMS/Call/Telegram, because the on-call timeline is the one surface
40
+ * a responder and an operator both read.
41
+ *
42
+ * 4. FAILURE ISOLATION. A failed WORKSPACE LOG write must never turn a
43
+ * delivered page into a reported failure, and a failed send must still be
44
+ * thrown to the caller after the log and timeline writes happen.
45
+ */
46
+
47
+ const PROJECT_ID: ObjectID = new ObjectID(
48
+ "11111111-1111-4111-8111-111111111111",
49
+ );
50
+ const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
51
+ const TIMELINE_ID: ObjectID = new ObjectID(
52
+ "33333333-3333-4333-8333-333333333333",
53
+ );
54
+ const INCIDENT_ID: ObjectID = new ObjectID(
55
+ "44444444-4444-4444-8444-444444444444",
56
+ );
57
+
58
+ const SLACK_USER_ID: string = "U0123ABCD";
59
+ const TEAMS_USER_ID: string = "aad-object-id-1";
60
+ const BOT_TOKEN: string = "xoxb-project-bot-token";
61
+
62
+ function markdownBlocks(): Array<WorkspaceMessageBlock> {
63
+ const block: WorkspacePayloadMarkdown = {
64
+ _type: "WorkspacePayloadMarkdown",
65
+ text: "**incident** page body",
66
+ };
67
+
68
+ return [block];
69
+ }
70
+
71
+ function projectAuth(): WorkspaceProjectAuthToken {
72
+ return {
73
+ id: PROJECT_ID,
74
+ authToken: BOT_TOKEN,
75
+ } as unknown as WorkspaceProjectAuthToken;
76
+ }
77
+
78
+ describe("WorkspaceUserNotificationService.sendDirectMessageToUser", () => {
79
+ let getProjectAuth: jest.SpyInstance;
80
+ let slackSend: jest.SpyInstance;
81
+ let teamsSend: jest.SpyInstance;
82
+ let createLog: jest.SpyInstance;
83
+ let updateTimeline: jest.SpyInstance;
84
+
85
+ beforeEach(() => {
86
+ jest.spyOn(logger, "error").mockImplementation((): void => {
87
+ return undefined;
88
+ });
89
+
90
+ getProjectAuth = jest
91
+ .spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
92
+ .mockResolvedValue(projectAuth() as never);
93
+
94
+ slackSend = jest
95
+ .spyOn(SlackUtil, "sendDirectMessageToUser")
96
+ .mockResolvedValue(undefined as never);
97
+
98
+ teamsSend = jest
99
+ .spyOn(MicrosoftTeamsUtil, "sendDirectMessageToUserAsBot")
100
+ .mockResolvedValue(undefined as never);
101
+
102
+ createLog = jest
103
+ .spyOn(WorkspaceNotificationLogService, "createWorkspaceLog")
104
+ .mockResolvedValue({} as never);
105
+
106
+ updateTimeline = jest
107
+ .spyOn(UserOnCallLogTimelineService, "updateOneById")
108
+ .mockResolvedValue(undefined as never);
109
+ });
110
+
111
+ afterEach(() => {
112
+ jest.restoreAllMocks();
113
+ });
114
+
115
+ /*
116
+ * ----------------------------------------------------------------------- *
117
+ * (A) Credential resolution.
118
+ * -----------------------------------------------------------------------
119
+ */
120
+
121
+ describe("credential resolution", () => {
122
+ test("resolves the PROJECT auth token for the given workspace type", async () => {
123
+ await WorkspaceUserNotificationService.sendDirectMessageToUser({
124
+ projectId: PROJECT_ID,
125
+ workspaceType: WorkspaceType.Slack,
126
+ workspaceUserId: SLACK_USER_ID,
127
+ messageBlocks: markdownBlocks(),
128
+ });
129
+
130
+ expect(getProjectAuth).toHaveBeenCalledTimes(1);
131
+ const arg: { projectId: ObjectID; workspaceType: WorkspaceType } =
132
+ getProjectAuth.mock.calls[0][0] as {
133
+ projectId: ObjectID;
134
+ workspaceType: WorkspaceType;
135
+ };
136
+ expect(arg.projectId.toString()).toBe(PROJECT_ID.toString());
137
+ expect(arg.workspaceType).toBe(WorkspaceType.Slack);
138
+ });
139
+
140
+ test("a project with no workspace connection throws the actionable message and sends nothing", async () => {
141
+ getProjectAuth.mockResolvedValue(null as never);
142
+
143
+ await expect(
144
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
145
+ projectId: PROJECT_ID,
146
+ workspaceType: WorkspaceType.Slack,
147
+ workspaceUserId: SLACK_USER_ID,
148
+ messageBlocks: markdownBlocks(),
149
+ }),
150
+ ).rejects.toThrow(
151
+ "This project is not connected to Slack. Please go to Project Settings and connect the account.",
152
+ );
153
+
154
+ expect(slackSend).not.toHaveBeenCalled();
155
+ expect(teamsSend).not.toHaveBeenCalled();
156
+ });
157
+
158
+ test("a project auth row with an empty token is treated as not connected", async () => {
159
+ getProjectAuth.mockResolvedValue({
160
+ id: PROJECT_ID,
161
+ authToken: undefined,
162
+ } as unknown as WorkspaceProjectAuthToken as never);
163
+
164
+ await expect(
165
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
166
+ projectId: PROJECT_ID,
167
+ workspaceType: WorkspaceType.MicrosoftTeams,
168
+ workspaceUserId: TEAMS_USER_ID,
169
+ messageBlocks: markdownBlocks(),
170
+ }),
171
+ ).rejects.toThrow(
172
+ "This project is not connected to Microsoft Teams. Please go to Project Settings and connect the account.",
173
+ );
174
+
175
+ expect(teamsSend).not.toHaveBeenCalled();
176
+ });
177
+
178
+ test("an empty workspaceUserId throws the user-side actionable message before any lookup", async () => {
179
+ await expect(
180
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
181
+ projectId: PROJECT_ID,
182
+ workspaceType: WorkspaceType.Slack,
183
+ workspaceUserId: "",
184
+ messageBlocks: markdownBlocks(),
185
+ }),
186
+ ).rejects.toThrow(
187
+ "This account is not connected to Slack. Please go to User Settings and connect the account.",
188
+ );
189
+
190
+ expect(getProjectAuth).not.toHaveBeenCalled();
191
+ expect(slackSend).not.toHaveBeenCalled();
192
+ });
193
+ });
194
+
195
+ /*
196
+ * ----------------------------------------------------------------------- *
197
+ * (B) Per-platform dispatch.
198
+ * -----------------------------------------------------------------------
199
+ */
200
+
201
+ describe("per-platform dispatch", () => {
202
+ test("Slack goes through SlackUtil with the PROJECT bot token and the member id", async () => {
203
+ const blocks: Array<WorkspaceMessageBlock> = markdownBlocks();
204
+
205
+ await WorkspaceUserNotificationService.sendDirectMessageToUser({
206
+ projectId: PROJECT_ID,
207
+ workspaceType: WorkspaceType.Slack,
208
+ workspaceUserId: SLACK_USER_ID,
209
+ messageBlocks: blocks,
210
+ });
211
+
212
+ expect(slackSend).toHaveBeenCalledTimes(1);
213
+ const arg: {
214
+ authToken: string;
215
+ workspaceUserId: string;
216
+ messageBlocks: Array<WorkspaceMessageBlock>;
217
+ } = slackSend.mock.calls[0][0] as {
218
+ authToken: string;
219
+ workspaceUserId: string;
220
+ messageBlocks: Array<WorkspaceMessageBlock>;
221
+ };
222
+ expect(arg.authToken).toBe(BOT_TOKEN);
223
+ expect(arg.workspaceUserId).toBe(SLACK_USER_ID);
224
+ expect(arg.messageBlocks).toBe(blocks);
225
+
226
+ expect(teamsSend).not.toHaveBeenCalled();
227
+ });
228
+
229
+ test("Microsoft Teams goes through the Bot Framework helper with the projectId, not a token", async () => {
230
+ const blocks: Array<WorkspaceMessageBlock> = markdownBlocks();
231
+
232
+ await WorkspaceUserNotificationService.sendDirectMessageToUser({
233
+ projectId: PROJECT_ID,
234
+ workspaceType: WorkspaceType.MicrosoftTeams,
235
+ workspaceUserId: TEAMS_USER_ID,
236
+ messageBlocks: blocks,
237
+ });
238
+
239
+ expect(teamsSend).toHaveBeenCalledTimes(1);
240
+ const arg: {
241
+ projectId: ObjectID;
242
+ workspaceUserId: string;
243
+ messageBlocks: Array<WorkspaceMessageBlock>;
244
+ } = teamsSend.mock.calls[0][0] as {
245
+ projectId: ObjectID;
246
+ workspaceUserId: string;
247
+ messageBlocks: Array<WorkspaceMessageBlock>;
248
+ };
249
+ expect(arg.projectId.toString()).toBe(PROJECT_ID.toString());
250
+ expect(arg.workspaceUserId).toBe(TEAMS_USER_ID);
251
+ expect(arg.messageBlocks).toBe(blocks);
252
+
253
+ expect(slackSend).not.toHaveBeenCalled();
254
+ });
255
+ });
256
+
257
+ /*
258
+ * ----------------------------------------------------------------------- *
259
+ * (C) The on-call timeline contract.
260
+ * -----------------------------------------------------------------------
261
+ */
262
+
263
+ describe("the on-call timeline contract", () => {
264
+ test("a successful send flips the timeline row to Sent, as root", async () => {
265
+ await WorkspaceUserNotificationService.sendDirectMessageToUser({
266
+ projectId: PROJECT_ID,
267
+ workspaceType: WorkspaceType.Slack,
268
+ workspaceUserId: SLACK_USER_ID,
269
+ messageBlocks: markdownBlocks(),
270
+ userOnCallLogTimelineId: TIMELINE_ID,
271
+ });
272
+
273
+ expect(updateTimeline).toHaveBeenCalledTimes(1);
274
+ const arg: {
275
+ id: ObjectID;
276
+ data: { status: UserNotificationStatus; statusMessage: string };
277
+ props: { isRoot: boolean };
278
+ } = updateTimeline.mock.calls[0][0] as {
279
+ id: ObjectID;
280
+ data: { status: UserNotificationStatus; statusMessage: string };
281
+ props: { isRoot: boolean };
282
+ };
283
+ expect(arg.id.toString()).toBe(TIMELINE_ID.toString());
284
+ expect(arg.data.status).toBe(UserNotificationStatus.Sent);
285
+ expect(arg.data.statusMessage).toBe("Message sent on Slack.");
286
+ expect(arg.props.isRoot).toBe(true);
287
+ });
288
+
289
+ test("a failed send flips the timeline row to Error carrying the send error's own message", async () => {
290
+ slackSend.mockRejectedValue(new Error("channel_not_found") as never);
291
+
292
+ await expect(
293
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
294
+ projectId: PROJECT_ID,
295
+ workspaceType: WorkspaceType.Slack,
296
+ workspaceUserId: SLACK_USER_ID,
297
+ messageBlocks: markdownBlocks(),
298
+ userOnCallLogTimelineId: TIMELINE_ID,
299
+ }),
300
+ ).rejects.toThrow("channel_not_found");
301
+
302
+ const arg: {
303
+ data: { status: UserNotificationStatus; statusMessage: string };
304
+ } = updateTimeline.mock.calls[0][0] as {
305
+ data: { status: UserNotificationStatus; statusMessage: string };
306
+ };
307
+ expect(arg.data.status).toBe(UserNotificationStatus.Error);
308
+ expect(arg.data.statusMessage).toBe("channel_not_found");
309
+ });
310
+
311
+ test("a missing-connection failure also lands on the timeline, with the actionable message", async () => {
312
+ getProjectAuth.mockResolvedValue(null as never);
313
+
314
+ await expect(
315
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
316
+ projectId: PROJECT_ID,
317
+ workspaceType: WorkspaceType.Slack,
318
+ workspaceUserId: SLACK_USER_ID,
319
+ messageBlocks: markdownBlocks(),
320
+ userOnCallLogTimelineId: TIMELINE_ID,
321
+ }),
322
+ ).rejects.toThrow(BadDataException);
323
+
324
+ const arg: {
325
+ data: { status: UserNotificationStatus; statusMessage: string };
326
+ } = updateTimeline.mock.calls[0][0] as {
327
+ data: { status: UserNotificationStatus; statusMessage: string };
328
+ };
329
+ expect(arg.data.status).toBe(UserNotificationStatus.Error);
330
+ expect(arg.data.statusMessage).toContain(
331
+ "This project is not connected to Slack",
332
+ );
333
+ });
334
+
335
+ test("no timeline id means no timeline write - the settings path has no row to flip", async () => {
336
+ await WorkspaceUserNotificationService.sendDirectMessageToUser({
337
+ projectId: PROJECT_ID,
338
+ workspaceType: WorkspaceType.Slack,
339
+ workspaceUserId: SLACK_USER_ID,
340
+ messageBlocks: markdownBlocks(),
341
+ });
342
+
343
+ expect(updateTimeline).not.toHaveBeenCalled();
344
+ });
345
+ });
346
+
347
+ /*
348
+ * ----------------------------------------------------------------------- *
349
+ * (D) The workspace notification log.
350
+ * -----------------------------------------------------------------------
351
+ */
352
+
353
+ describe("the workspace notification log", () => {
354
+ test("a successful send writes one Success SendMessage row carrying the context ids", async () => {
355
+ await WorkspaceUserNotificationService.sendDirectMessageToUser({
356
+ projectId: PROJECT_ID,
357
+ workspaceType: WorkspaceType.Slack,
358
+ workspaceUserId: SLACK_USER_ID,
359
+ messageBlocks: markdownBlocks(),
360
+ messageSummary: "On-call notification: incident created.",
361
+ userId: USER_ID,
362
+ incidentId: INCIDENT_ID,
363
+ });
364
+
365
+ expect(createLog).toHaveBeenCalledTimes(1);
366
+ const logData: {
367
+ projectId: ObjectID;
368
+ workspaceType: WorkspaceType;
369
+ actionType: WorkspaceNotificationActionType;
370
+ status: WorkspaceNotificationStatus;
371
+ message: string;
372
+ userId: ObjectID;
373
+ incidentId: ObjectID;
374
+ } = createLog.mock.calls[0][0] as {
375
+ projectId: ObjectID;
376
+ workspaceType: WorkspaceType;
377
+ actionType: WorkspaceNotificationActionType;
378
+ status: WorkspaceNotificationStatus;
379
+ message: string;
380
+ userId: ObjectID;
381
+ incidentId: ObjectID;
382
+ };
383
+ expect(logData.projectId.toString()).toBe(PROJECT_ID.toString());
384
+ expect(logData.workspaceType).toBe(WorkspaceType.Slack);
385
+ expect(logData.actionType).toBe(
386
+ WorkspaceNotificationActionType.SendMessage,
387
+ );
388
+ expect(logData.status).toBe(WorkspaceNotificationStatus.Success);
389
+ expect(logData.message).toBe("On-call notification: incident created.");
390
+ expect(logData.userId.toString()).toBe(USER_ID.toString());
391
+ expect(logData.incidentId.toString()).toBe(INCIDENT_ID.toString());
392
+ });
393
+
394
+ test("a failed send writes one Error row whose statusMessage is the send error", async () => {
395
+ teamsSend.mockRejectedValue(new Error("app not installed") as never);
396
+
397
+ await expect(
398
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
399
+ projectId: PROJECT_ID,
400
+ workspaceType: WorkspaceType.MicrosoftTeams,
401
+ workspaceUserId: TEAMS_USER_ID,
402
+ messageBlocks: markdownBlocks(),
403
+ }),
404
+ ).rejects.toThrow("app not installed");
405
+
406
+ const logData: {
407
+ status: WorkspaceNotificationStatus;
408
+ statusMessage: string;
409
+ } = createLog.mock.calls[0][0] as {
410
+ status: WorkspaceNotificationStatus;
411
+ statusMessage: string;
412
+ };
413
+ expect(logData.status).toBe(WorkspaceNotificationStatus.Error);
414
+ expect(logData.statusMessage).toBe("app not installed");
415
+ });
416
+
417
+ /*
418
+ * The isolation property: the log is observability, not delivery. A log
419
+ * write that raises must neither fail a delivered page nor mask the real
420
+ * error of a failed one.
421
+ */
422
+ test("a failed LOG write does not fail a delivered send", async () => {
423
+ createLog.mockRejectedValue(new Error("log table gone") as never);
424
+
425
+ await expect(
426
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
427
+ projectId: PROJECT_ID,
428
+ workspaceType: WorkspaceType.Slack,
429
+ workspaceUserId: SLACK_USER_ID,
430
+ messageBlocks: markdownBlocks(),
431
+ userOnCallLogTimelineId: TIMELINE_ID,
432
+ }),
433
+ ).resolves.toBeUndefined();
434
+
435
+ // The timeline still gets its Sent flip despite the log failure.
436
+ const arg: { data: { status: UserNotificationStatus } } = updateTimeline
437
+ .mock.calls[0][0] as {
438
+ data: { status: UserNotificationStatus };
439
+ };
440
+ expect(arg.data.status).toBe(UserNotificationStatus.Sent);
441
+ });
442
+
443
+ test("a failed LOG write does not mask a failed send's own error", async () => {
444
+ slackSend.mockRejectedValue(new Error("real send failure") as never);
445
+ createLog.mockRejectedValue(new Error("log table gone") as never);
446
+
447
+ await expect(
448
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
449
+ projectId: PROJECT_ID,
450
+ workspaceType: WorkspaceType.Slack,
451
+ workspaceUserId: SLACK_USER_ID,
452
+ messageBlocks: markdownBlocks(),
453
+ }),
454
+ ).rejects.toThrow("real send failure");
455
+ });
456
+ });
457
+
458
+ /*
459
+ * ----------------------------------------------------------------------- *
460
+ * (E) Rethrow semantics.
461
+ * -----------------------------------------------------------------------
462
+ */
463
+
464
+ describe("rethrow semantics", () => {
465
+ test("the send error is rethrown AFTER the log and timeline writes have happened", async () => {
466
+ slackSend.mockRejectedValue(new Error("slack 500") as never);
467
+
468
+ await expect(
469
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
470
+ projectId: PROJECT_ID,
471
+ workspaceType: WorkspaceType.Slack,
472
+ workspaceUserId: SLACK_USER_ID,
473
+ messageBlocks: markdownBlocks(),
474
+ userOnCallLogTimelineId: TIMELINE_ID,
475
+ }),
476
+ ).rejects.toThrow("slack 500");
477
+
478
+ expect(createLog).toHaveBeenCalledTimes(1);
479
+ expect(updateTimeline).toHaveBeenCalledTimes(1);
480
+ });
481
+
482
+ test("an unsupported workspace type is refused without sending anywhere", async () => {
483
+ await expect(
484
+ WorkspaceUserNotificationService.sendDirectMessageToUser({
485
+ projectId: PROJECT_ID,
486
+ workspaceType: "Discord" as unknown as WorkspaceType,
487
+ workspaceUserId: "someone",
488
+ messageBlocks: markdownBlocks(),
489
+ }),
490
+ ).rejects.toThrow(BadDataException);
491
+
492
+ expect(slackSend).not.toHaveBeenCalled();
493
+ expect(teamsSend).not.toHaveBeenCalled();
494
+ });
495
+ });
496
+ });