@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,669 @@
1
+ import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
2
+ import UserOnCallLogService from "../../../Server/Services/UserOnCallLogService";
3
+ import UserOnCallLogTimelineService from "../../../Server/Services/UserOnCallLogTimelineService";
4
+ import ProjectCallSMSConfigService from "../../../Server/Services/ProjectCallSMSConfigService";
5
+ import IncidentService from "../../../Server/Services/IncidentService";
6
+ import AlertService from "../../../Server/Services/AlertService";
7
+ import AlertEpisodeService from "../../../Server/Services/AlertEpisodeService";
8
+ import IncidentEpisodeService from "../../../Server/Services/IncidentEpisodeService";
9
+ import WorkspaceUserNotificationService from "../../../Server/Services/WorkspaceUserNotificationService";
10
+ import ShortLinkService from "../../../Server/Services/ShortLinkService";
11
+ import DatabaseConfig from "../../../Server/DatabaseConfig";
12
+ import logger from "../../../Server/Utils/Logger";
13
+ import Hostname from "../../../Types/API/Hostname";
14
+ import Protocol from "../../../Types/API/Protocol";
15
+ import URL from "../../../Types/API/URL";
16
+ import Incident from "../../../Models/DatabaseModels/Incident";
17
+ import Alert from "../../../Models/DatabaseModels/Alert";
18
+ import AlertEpisode from "../../../Models/DatabaseModels/AlertEpisode";
19
+ import IncidentEpisode from "../../../Models/DatabaseModels/IncidentEpisode";
20
+ import ShortLink from "../../../Models/DatabaseModels/ShortLink";
21
+ import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
22
+ import UserOnCallLogTimeline from "../../../Models/DatabaseModels/UserOnCallLogTimeline";
23
+ import ObjectID from "../../../Types/ObjectID";
24
+ import { JSONObject } from "../../../Types/JSON";
25
+ import UserNotificationEventType from "../../../Types/UserNotification/UserNotificationEventType";
26
+ import UserNotificationStatus from "../../../Types/UserNotification/UserNotificationStatus";
27
+ import {
28
+ WorkspaceMessageBlock,
29
+ WorkspacePayloadMarkdown,
30
+ } from "../../../Types/Workspace/WorkspaceMessagePayload";
31
+ import WorkspaceType from "../../../Types/Workspace/WorkspaceType";
32
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
33
+
34
+ /*
35
+ * The Slack / Microsoft Teams halves of deliverNotificationForRule, driven
36
+ * through the public executeNotificationRuleItem entrypoint exactly like the
37
+ * older channels' characterisation suite. What is pinned:
38
+ *
39
+ * 1. THE VERIFICATION GATE. A verified method with an address delivers; an
40
+ * unverified one writes exactly one "not verified" Error row and never
41
+ * reaches the sender; a verified relation whose address column was lost
42
+ * does neither (the same silent-skip every channel has).
43
+ *
44
+ * 2. THE TIMELINE ROW. Written BEFORE the send (the message embeds its id
45
+ * via the acknowledge link), stamped with the right method FK column,
46
+ * status Sending, and "Sending Slack message." / "Sending Microsoft
47
+ * Teams message." as its message.
48
+ *
49
+ * 3. THE HANDOFF. The sender is handed the workspace type, the stored
50
+ * workspace user id, the timeline row id (so it can flip Sent/Error
51
+ * itself), and the correlation ids of whatever fired.
52
+ *
53
+ * 4. FIRE-AND-FORGET. A sender rejection is caught and lands on the
54
+ * timeline row as Error; it never rejects the execution.
55
+ *
56
+ * 5. THE EVENT MAP. All four event types deliver on both channels, and the
57
+ * blocks handed over are the event's own generator output (markdown with
58
+ * the acknowledge short-link inside).
59
+ */
60
+
61
+ const PROJECT_ID: ObjectID = new ObjectID(
62
+ "11111111-1111-4111-8111-111111111111",
63
+ );
64
+ const RULE_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
65
+ const LOG_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
66
+ const USER_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
67
+ const INCIDENT_ID: ObjectID = new ObjectID(
68
+ "55555555-5555-4555-8555-555555555555",
69
+ );
70
+ const ALERT_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
71
+ const ALERT_EPISODE_ID: ObjectID = new ObjectID(
72
+ "77777777-7777-4777-8777-777777777777",
73
+ );
74
+ const INCIDENT_EPISODE_ID: ObjectID = new ObjectID(
75
+ "88888888-8888-4888-8888-888888888888",
76
+ );
77
+ const METHOD_ID: ObjectID = new ObjectID(
78
+ "99999999-9999-4999-8999-999999999999",
79
+ );
80
+ const TIMELINE_ID: ObjectID = new ObjectID(
81
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
82
+ );
83
+ const POLICY_ID: ObjectID = new ObjectID(
84
+ "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
85
+ );
86
+ const ESCALATION_RULE_ID: ObjectID = new ObjectID(
87
+ "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
88
+ );
89
+ const TEAM_ID: ObjectID = new ObjectID("dddddddd-dddd-4ddd-8ddd-dddddddddddd");
90
+ const SCHEDULE_ID: ObjectID = new ObjectID(
91
+ "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
92
+ );
93
+
94
+ const SLACK_USER_ID: string = "U0123ABCD";
95
+ const TEAMS_USER_ID: string = "entra-object-id-1";
96
+ const ACK_SHORT_URL: string = "https://oneuptime.example.com/l/abc123";
97
+
98
+ type ExecuteOptions = Parameters<
99
+ typeof UserNotificationRuleService.executeNotificationRuleItem
100
+ >[1];
101
+
102
+ interface TimelineRow {
103
+ status: UserNotificationStatus | undefined;
104
+ statusMessage: string | undefined;
105
+ userSlackId: ObjectID | undefined;
106
+ userMicrosoftTeamsId: ObjectID | undefined;
107
+ }
108
+
109
+ function flushMicrotasks(): Promise<void> {
110
+ return Promise.resolve()
111
+ .then((): Promise<void> => {
112
+ return Promise.resolve();
113
+ })
114
+ .then((): Promise<void> => {
115
+ return Promise.resolve();
116
+ });
117
+ }
118
+
119
+ function executeOptions(
120
+ overrides: Partial<ExecuteOptions> = {},
121
+ ): ExecuteOptions {
122
+ return {
123
+ projectId: PROJECT_ID,
124
+ userNotificationEventType: UserNotificationEventType.IncidentCreated,
125
+ triggeredByIncidentId: INCIDENT_ID,
126
+ onCallPolicyId: POLICY_ID,
127
+ onCallPolicyEscalationRuleId: ESCALATION_RULE_ID,
128
+ userBelongsToTeamId: TEAM_ID,
129
+ onCallScheduleId: SCHEDULE_ID,
130
+ userNotificationLogId: LOG_ID,
131
+ ...overrides,
132
+ } as ExecuteOptions;
133
+ }
134
+
135
+ function ruleItem(channels: JSONObject = {}): UserNotificationRule {
136
+ return {
137
+ id: RULE_ID,
138
+ _id: RULE_ID.toString(),
139
+ userId: USER_ID,
140
+ ...channels,
141
+ } as unknown as UserNotificationRule;
142
+ }
143
+
144
+ function verifiedSlack(): JSONObject {
145
+ return {
146
+ userSlack: {
147
+ id: METHOD_ID,
148
+ slackUserId: SLACK_USER_ID,
149
+ slackUserName: "alice",
150
+ isVerified: true,
151
+ },
152
+ } as unknown as JSONObject;
153
+ }
154
+
155
+ function verifiedTeams(): JSONObject {
156
+ return {
157
+ userMicrosoftTeams: {
158
+ id: METHOD_ID,
159
+ microsoftTeamsUserId: TEAMS_USER_ID,
160
+ microsoftTeamsUserName: "Alice Example",
161
+ isVerified: true,
162
+ },
163
+ } as unknown as JSONObject;
164
+ }
165
+
166
+ describe("deliverNotificationForRule - Slack and Microsoft Teams", () => {
167
+ let findRule: jest.SpyInstance;
168
+ let sendDm: jest.SpyInstance;
169
+ let timelineCreate: jest.SpyInstance;
170
+ let timelineUpdate: jest.SpyInstance;
171
+ let timelineRows: Array<TimelineRow>;
172
+
173
+ beforeEach(() => {
174
+ timelineRows = [];
175
+
176
+ jest.spyOn(logger, "error").mockImplementation((): void => {
177
+ return undefined;
178
+ });
179
+
180
+ jest
181
+ .spyOn(UserOnCallLogService, "claimNotificationRuleExecution")
182
+ .mockResolvedValue(true as never);
183
+
184
+ findRule = jest
185
+ .spyOn(UserNotificationRuleService, "findOneById")
186
+ .mockResolvedValue(ruleItem(verifiedSlack()) as never);
187
+
188
+ jest
189
+ .spyOn(ProjectCallSMSConfigService, "getProjectDefaultTwilioConfig")
190
+ .mockResolvedValue(undefined as never);
191
+
192
+ jest.spyOn(IncidentService, "findOneById").mockResolvedValue({
193
+ id: INCIDENT_ID,
194
+ projectId: PROJECT_ID,
195
+ title: "Checkout is down",
196
+ incidentNumber: 42,
197
+ incidentNumberWithPrefix: "INC-42",
198
+ } as unknown as Incident as never);
199
+
200
+ jest.spyOn(AlertService, "findOneById").mockResolvedValue({
201
+ id: ALERT_ID,
202
+ projectId: PROJECT_ID,
203
+ title: "Disk almost full",
204
+ alertNumber: 7,
205
+ alertNumberWithPrefix: "ALR-7",
206
+ } as unknown as Alert as never);
207
+
208
+ jest.spyOn(AlertEpisodeService, "findOneById").mockResolvedValue({
209
+ id: ALERT_EPISODE_ID,
210
+ projectId: PROJECT_ID,
211
+ title: "Flapping disk alerts",
212
+ episodeNumber: 3,
213
+ episodeNumberWithPrefix: "AEP-3",
214
+ } as unknown as AlertEpisode as never);
215
+
216
+ jest.spyOn(IncidentEpisodeService, "findOneById").mockResolvedValue({
217
+ id: INCIDENT_EPISODE_ID,
218
+ projectId: PROJECT_ID,
219
+ title: "Checkout instability",
220
+ episodeNumber: 9,
221
+ episodeNumberWithPrefix: "IEP-9",
222
+ } as unknown as IncidentEpisode as never);
223
+
224
+ /*
225
+ * The dashboard-link and short-link plumbing the block generators sit on.
226
+ * Stubbing at this level (rather than stubbing the generators) keeps the
227
+ * real generator code in the loop, so the assertions below are about what
228
+ * an actual page carries.
229
+ */
230
+ jest
231
+ .spyOn(DatabaseConfig, "getHost")
232
+ .mockResolvedValue(new Hostname("oneuptime.example.com") as never);
233
+ jest
234
+ .spyOn(DatabaseConfig, "getHttpProtocol")
235
+ .mockResolvedValue(Protocol.HTTPS as never);
236
+ jest
237
+ .spyOn(ShortLinkService, "saveShortLinkFor")
238
+ .mockResolvedValue({} as ShortLink as never);
239
+ jest
240
+ .spyOn(ShortLinkService, "getShortenedUrl")
241
+ .mockResolvedValue(URL.fromString(ACK_SHORT_URL) as never);
242
+ jest
243
+ .spyOn(IncidentService, "getIncidentLinkInDashboard")
244
+ .mockResolvedValue(
245
+ URL.fromString(
246
+ "https://oneuptime.example.com/dashboard/incident",
247
+ ) as never,
248
+ );
249
+ jest
250
+ .spyOn(AlertService, "getAlertLinkInDashboard")
251
+ .mockResolvedValue(
252
+ URL.fromString(
253
+ "https://oneuptime.example.com/dashboard/alert",
254
+ ) as never,
255
+ );
256
+ jest
257
+ .spyOn(AlertEpisodeService, "getEpisodeLinkInDashboard")
258
+ .mockResolvedValue(
259
+ URL.fromString(
260
+ "https://oneuptime.example.com/dashboard/alert-episode",
261
+ ) as never,
262
+ );
263
+ jest
264
+ .spyOn(IncidentEpisodeService, "getEpisodeLinkInDashboard")
265
+ .mockResolvedValue(
266
+ URL.fromString(
267
+ "https://oneuptime.example.com/dashboard/incident-episode",
268
+ ) as never,
269
+ );
270
+
271
+ sendDm = jest
272
+ .spyOn(WorkspaceUserNotificationService, "sendDirectMessageToUser")
273
+ .mockResolvedValue(undefined as never);
274
+
275
+ timelineCreate = jest.spyOn(UserOnCallLogTimelineService, "create");
276
+ timelineCreate.mockImplementation(
277
+ (createByArg: unknown): Promise<UserOnCallLogTimeline> => {
278
+ const data: UserOnCallLogTimeline = (
279
+ createByArg as { data: UserOnCallLogTimeline }
280
+ ).data;
281
+
282
+ timelineRows.push({
283
+ status: data.status,
284
+ statusMessage: data.statusMessage,
285
+ userSlackId: data.userSlackId,
286
+ userMicrosoftTeamsId: data.userMicrosoftTeamsId,
287
+ });
288
+
289
+ return Promise.resolve({
290
+ id: TIMELINE_ID,
291
+ } as unknown as UserOnCallLogTimeline);
292
+ },
293
+ );
294
+
295
+ timelineUpdate = jest
296
+ .spyOn(UserOnCallLogTimelineService, "updateOneById")
297
+ .mockResolvedValue(undefined as never);
298
+ });
299
+
300
+ afterEach(() => {
301
+ jest.restoreAllMocks();
302
+ });
303
+
304
+ function execute(overrides: Partial<ExecuteOptions> = {}): Promise<void> {
305
+ return UserNotificationRuleService.executeNotificationRuleItem(
306
+ RULE_ID,
307
+ executeOptions(overrides),
308
+ );
309
+ }
310
+
311
+ interface CapturedSendArg {
312
+ projectId: ObjectID;
313
+ workspaceType: WorkspaceType;
314
+ workspaceUserId: string;
315
+ messageBlocks: Array<WorkspaceMessageBlock>;
316
+ userId: ObjectID;
317
+ userOnCallLogTimelineId: ObjectID;
318
+ incidentId?: ObjectID;
319
+ alertId?: ObjectID;
320
+ alertEpisodeId?: ObjectID;
321
+ incidentEpisodeId?: ObjectID;
322
+ onCallPolicyId?: ObjectID;
323
+ onCallPolicyEscalationRuleId?: ObjectID;
324
+ teamId?: ObjectID;
325
+ onCallScheduleId?: ObjectID;
326
+ }
327
+
328
+ function sendArg(): CapturedSendArg {
329
+ return sendDm.mock.calls[0][0] as CapturedSendArg;
330
+ }
331
+
332
+ function markdownText(): string {
333
+ const blocks: Array<WorkspaceMessageBlock> = sendArg().messageBlocks;
334
+ return (blocks[0] as WorkspacePayloadMarkdown).text;
335
+ }
336
+
337
+ /*
338
+ * ----------------------------------------------------------------------- *
339
+ * (A) The Slack happy path, incident created.
340
+ * -----------------------------------------------------------------------
341
+ */
342
+
343
+ describe("a verified Slack method on an IncidentCreated page", () => {
344
+ test("creates the timeline row FIRST (Sending, stamped with userSlackId), then sends", async () => {
345
+ await execute();
346
+
347
+ expect(timelineRows).toHaveLength(1);
348
+ expect(timelineRows[0]?.status).toBe(UserNotificationStatus.Sending);
349
+ expect(timelineRows[0]?.statusMessage).toBe("Sending Slack message.");
350
+ expect(timelineRows[0]?.userSlackId?.toString()).toBe(
351
+ METHOD_ID.toString(),
352
+ );
353
+
354
+ expect(timelineCreate.mock.invocationCallOrder[0] as number).toBeLessThan(
355
+ sendDm.mock.invocationCallOrder[0] as number,
356
+ );
357
+ });
358
+
359
+ test("hands the sender the Slack workspace type, stored member id, and the timeline id", async () => {
360
+ await execute();
361
+
362
+ expect(sendDm).toHaveBeenCalledTimes(1);
363
+ const arg: CapturedSendArg = sendArg();
364
+ expect(arg.workspaceType).toBe(WorkspaceType.Slack);
365
+ expect(arg.workspaceUserId).toBe(SLACK_USER_ID);
366
+ expect(arg.userOnCallLogTimelineId.toString()).toBe(
367
+ TIMELINE_ID.toString(),
368
+ );
369
+ expect(arg.userId.toString()).toBe(USER_ID.toString());
370
+ expect(arg.projectId.toString()).toBe(PROJECT_ID.toString());
371
+ });
372
+
373
+ test("forwards every on-call correlation id the send should be attributed to", async () => {
374
+ await execute();
375
+
376
+ const arg: CapturedSendArg = sendArg();
377
+ expect(arg.incidentId?.toString()).toBe(INCIDENT_ID.toString());
378
+ expect(arg.onCallPolicyId?.toString()).toBe(POLICY_ID.toString());
379
+ expect(arg.onCallPolicyEscalationRuleId?.toString()).toBe(
380
+ ESCALATION_RULE_ID.toString(),
381
+ );
382
+ expect(arg.teamId?.toString()).toBe(TEAM_ID.toString());
383
+ expect(arg.onCallScheduleId?.toString()).toBe(SCHEDULE_ID.toString());
384
+ });
385
+
386
+ test("the message is markdown carrying the incident identifier and the acknowledge short-link", async () => {
387
+ await execute();
388
+
389
+ const text: string = markdownText();
390
+ expect(text).toContain("New incident assigned to you");
391
+ expect(text).toContain("INC-42");
392
+ expect(text).toContain("Checkout is down");
393
+ expect(text).toContain("You're getting this because you're on call.");
394
+ expect(text).toContain(ACK_SHORT_URL);
395
+ expect(text).toContain(
396
+ "https://oneuptime.example.com/dashboard/incident",
397
+ );
398
+ });
399
+
400
+ test("the message sticks to block types both platforms implement (markdown only)", async () => {
401
+ await execute();
402
+
403
+ for (const block of sendArg().messageBlocks) {
404
+ expect(block._type).toBe("WorkspacePayloadMarkdown");
405
+ }
406
+ });
407
+ });
408
+
409
+ /*
410
+ * ----------------------------------------------------------------------- *
411
+ * (B) The Microsoft Teams path.
412
+ * -----------------------------------------------------------------------
413
+ */
414
+
415
+ describe("a verified Microsoft Teams method", () => {
416
+ beforeEach(() => {
417
+ findRule.mockResolvedValue(ruleItem(verifiedTeams()) as never);
418
+ });
419
+
420
+ test("delivers with the MicrosoftTeams workspace type and the stored Entra id", async () => {
421
+ await execute();
422
+
423
+ const arg: CapturedSendArg = sendArg();
424
+ expect(arg.workspaceType).toBe(WorkspaceType.MicrosoftTeams);
425
+ expect(arg.workspaceUserId).toBe(TEAMS_USER_ID);
426
+ });
427
+
428
+ test("its timeline row is stamped with userMicrosoftTeamsId and the Teams sending message", async () => {
429
+ await execute();
430
+
431
+ expect(timelineRows).toHaveLength(1);
432
+ expect(timelineRows[0]?.statusMessage).toBe(
433
+ "Sending Microsoft Teams message.",
434
+ );
435
+ expect(timelineRows[0]?.userMicrosoftTeamsId?.toString()).toBe(
436
+ METHOD_ID.toString(),
437
+ );
438
+ expect(timelineRows[0]?.userSlackId).toBeUndefined();
439
+ });
440
+ });
441
+
442
+ /*
443
+ * ----------------------------------------------------------------------- *
444
+ * (C) The verification gate.
445
+ * -----------------------------------------------------------------------
446
+ */
447
+
448
+ describe("the verification gate", () => {
449
+ test("an unverified Slack method writes one 'not verified' Error row and never sends", async () => {
450
+ findRule.mockResolvedValue(
451
+ ruleItem({
452
+ userSlack: {
453
+ id: METHOD_ID,
454
+ slackUserId: SLACK_USER_ID,
455
+ isVerified: false,
456
+ },
457
+ } as unknown as JSONObject) as never,
458
+ );
459
+
460
+ await execute();
461
+
462
+ expect(sendDm).not.toHaveBeenCalled();
463
+ expect(timelineRows).toHaveLength(1);
464
+ expect(timelineRows[0]?.status).toBe(UserNotificationStatus.Error);
465
+ expect(timelineRows[0]?.statusMessage).toBe(
466
+ "Slack message not sent because the Slack account is not verified.",
467
+ );
468
+ expect(timelineRows[0]?.userSlackId?.toString()).toBe(
469
+ METHOD_ID.toString(),
470
+ );
471
+ });
472
+
473
+ test("an unverified Microsoft Teams method behaves the same, with its own message", async () => {
474
+ findRule.mockResolvedValue(
475
+ ruleItem({
476
+ userMicrosoftTeams: {
477
+ id: METHOD_ID,
478
+ microsoftTeamsUserId: TEAMS_USER_ID,
479
+ isVerified: false,
480
+ },
481
+ } as unknown as JSONObject) as never,
482
+ );
483
+
484
+ await execute();
485
+
486
+ expect(sendDm).not.toHaveBeenCalled();
487
+ expect(timelineRows[0]?.statusMessage).toBe(
488
+ "Microsoft Teams message not sent because the Microsoft Teams account is not verified.",
489
+ );
490
+ });
491
+
492
+ test("a verified relation whose address column is missing sends nothing (the same silent skip as every channel)", async () => {
493
+ findRule.mockResolvedValue(
494
+ ruleItem({
495
+ userSlack: {
496
+ id: METHOD_ID,
497
+ isVerified: true,
498
+ },
499
+ } as unknown as JSONObject) as never,
500
+ );
501
+
502
+ await execute();
503
+
504
+ expect(sendDm).not.toHaveBeenCalled();
505
+ expect(timelineRows).toHaveLength(0);
506
+ });
507
+
508
+ test("the unverified guard keys off the RELATION, not the address - a row with no member id still errors", async () => {
509
+ findRule.mockResolvedValue(
510
+ ruleItem({
511
+ userSlack: {
512
+ id: METHOD_ID,
513
+ isVerified: false,
514
+ },
515
+ } as unknown as JSONObject) as never,
516
+ );
517
+
518
+ await execute();
519
+
520
+ expect(timelineRows).toHaveLength(1);
521
+ expect(timelineRows[0]?.statusMessage).toContain("not verified");
522
+ });
523
+ });
524
+
525
+ /*
526
+ * ----------------------------------------------------------------------- *
527
+ * (D) Fire-and-forget failure handling.
528
+ * -----------------------------------------------------------------------
529
+ */
530
+
531
+ describe("fire-and-forget failure handling", () => {
532
+ test("a sender rejection is caught and flips the timeline row to Error with the sender's message", async () => {
533
+ sendDm.mockRejectedValue(new Error("slack workspace revoked") as never);
534
+
535
+ await expect(execute()).resolves.toBeUndefined();
536
+ await flushMicrotasks();
537
+
538
+ expect(timelineUpdate).toHaveBeenCalledTimes(1);
539
+ const arg: {
540
+ id: ObjectID;
541
+ data: { status: UserNotificationStatus; statusMessage: string };
542
+ } = timelineUpdate.mock.calls[0][0] as {
543
+ id: ObjectID;
544
+ data: { status: UserNotificationStatus; statusMessage: string };
545
+ };
546
+ expect(arg.id.toString()).toBe(TIMELINE_ID.toString());
547
+ expect(arg.data.status).toBe(UserNotificationStatus.Error);
548
+ expect(arg.data.statusMessage).toBe("slack workspace revoked");
549
+ });
550
+
551
+ test("a rejection with no message falls back to the channel-named default", async () => {
552
+ sendDm.mockRejectedValue(new Error("") as never);
553
+
554
+ await execute();
555
+ await flushMicrotasks();
556
+
557
+ const arg: { data: { statusMessage: string } } = timelineUpdate.mock
558
+ .calls[0][0] as { data: { statusMessage: string } };
559
+ expect(arg.data.statusMessage).toBe("Error sending Slack message.");
560
+ });
561
+ });
562
+
563
+ /*
564
+ * ----------------------------------------------------------------------- *
565
+ * (E) The event map — all four event types deliver, on the right entity.
566
+ * -----------------------------------------------------------------------
567
+ */
568
+
569
+ describe("the event map", () => {
570
+ test("AlertCreated delivers the alert message with the alert correlation id", async () => {
571
+ await execute({
572
+ userNotificationEventType: UserNotificationEventType.AlertCreated,
573
+ triggeredByIncidentId: undefined,
574
+ triggeredByAlertId: ALERT_ID,
575
+ });
576
+
577
+ const arg: CapturedSendArg = sendArg();
578
+ expect(arg.alertId?.toString()).toBe(ALERT_ID.toString());
579
+ expect(arg.incidentId).toBeUndefined();
580
+ expect(markdownText()).toContain("New alert assigned to you");
581
+ expect(markdownText()).toContain("ALR-7");
582
+ });
583
+
584
+ test("AlertEpisodeCreated delivers the alert-episode message", async () => {
585
+ await execute({
586
+ userNotificationEventType:
587
+ UserNotificationEventType.AlertEpisodeCreated,
588
+ triggeredByIncidentId: undefined,
589
+ triggeredByAlertEpisodeId: ALERT_EPISODE_ID,
590
+ });
591
+
592
+ const arg: CapturedSendArg = sendArg();
593
+ expect(arg.alertEpisodeId?.toString()).toBe(ALERT_EPISODE_ID.toString());
594
+ expect(markdownText()).toContain("New alert episode assigned to you");
595
+ expect(markdownText()).toContain("AEP-3");
596
+ });
597
+
598
+ test("IncidentEpisodeCreated delivers the incident-episode message", async () => {
599
+ await execute({
600
+ userNotificationEventType:
601
+ UserNotificationEventType.IncidentEpisodeCreated,
602
+ triggeredByIncidentId: undefined,
603
+ triggeredByIncidentEpisodeId: INCIDENT_EPISODE_ID,
604
+ });
605
+
606
+ const arg: CapturedSendArg = sendArg();
607
+ expect(arg.incidentEpisodeId?.toString()).toBe(
608
+ INCIDENT_EPISODE_ID.toString(),
609
+ );
610
+ expect(markdownText()).toContain("New incident episode assigned to you");
611
+ expect(markdownText()).toContain("IEP-9");
612
+ });
613
+
614
+ test("every event's message carries the acknowledge short-link - the whole point of writing the row first", async () => {
615
+ for (const eventOverrides of [
616
+ {
617
+ userNotificationEventType: UserNotificationEventType.AlertCreated,
618
+ triggeredByIncidentId: undefined,
619
+ triggeredByAlertId: ALERT_ID,
620
+ },
621
+ {
622
+ userNotificationEventType:
623
+ UserNotificationEventType.AlertEpisodeCreated,
624
+ triggeredByIncidentId: undefined,
625
+ triggeredByAlertEpisodeId: ALERT_EPISODE_ID,
626
+ },
627
+ {
628
+ userNotificationEventType:
629
+ UserNotificationEventType.IncidentEpisodeCreated,
630
+ triggeredByIncidentId: undefined,
631
+ triggeredByIncidentEpisodeId: INCIDENT_EPISODE_ID,
632
+ },
633
+ ] as Array<Partial<ExecuteOptions>>) {
634
+ sendDm.mockClear();
635
+ await execute(eventOverrides);
636
+ expect(markdownText()).toContain(ACK_SHORT_URL);
637
+ }
638
+ });
639
+ });
640
+
641
+ /*
642
+ * ----------------------------------------------------------------------- *
643
+ * (F) Both channels on one rule.
644
+ * -----------------------------------------------------------------------
645
+ */
646
+
647
+ describe("a rule carrying both workspace methods", () => {
648
+ test("delivers on both, one timeline row and one send per channel", async () => {
649
+ findRule.mockResolvedValue(
650
+ ruleItem({
651
+ ...verifiedSlack(),
652
+ ...verifiedTeams(),
653
+ }) as never,
654
+ );
655
+
656
+ await execute();
657
+
658
+ expect(sendDm).toHaveBeenCalledTimes(2);
659
+
660
+ const workspaceTypes: Array<WorkspaceType> = sendDm.mock.calls.map(
661
+ (call: Array<unknown>) => {
662
+ return (call[0] as { workspaceType: WorkspaceType }).workspaceType;
663
+ },
664
+ );
665
+ expect(workspaceTypes).toContain(WorkspaceType.Slack);
666
+ expect(workspaceTypes).toContain(WorkspaceType.MicrosoftTeams);
667
+ });
668
+ });
669
+ });