@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.
- package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +12 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +67 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Types/Marketing/MarketingEvent.ts +97 -0
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +10 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +56 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- 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
|
+
});
|