@oneuptime/common 12.0.28 → 12.0.29

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 (182) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  3. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  4. package/Server/API/DashboardDomainAPI.ts +74 -0
  5. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  6. package/Server/API/StatusPageDomainAPI.ts +74 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  11. package/Server/Services/DashboardDomainService.ts +131 -0
  12. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  13. package/Server/Services/MonitorTemplateService.ts +70 -11
  14. package/Server/Services/StatusPageDomainService.ts +163 -3
  15. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  16. package/Server/Types/Domain.ts +17 -0
  17. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  18. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  19. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  20. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  21. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  22. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  23. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  24. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  25. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  26. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  27. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  28. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  29. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  30. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  31. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  32. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  33. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  34. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  35. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  36. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  37. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  38. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  39. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  40. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  41. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  42. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  43. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  44. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  45. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  46. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  47. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  48. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  49. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  50. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  51. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  52. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  53. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  54. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  55. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  56. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  57. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  58. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  59. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  60. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  61. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  62. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  63. package/Tests/Types/API/Route.test.ts +36 -4
  64. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  65. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  66. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  67. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  68. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  69. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  70. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  71. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  72. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  73. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  74. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  75. package/Tests/Utils/Array.test.ts +100 -0
  76. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  77. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  79. package/Types/API/Route.ts +20 -5
  80. package/Types/Date.ts +143 -13
  81. package/Types/Rum/SessionReplay.ts +62 -1
  82. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  83. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  84. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  85. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  86. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  87. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  88. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  89. package/UI/Utils/Navigation.ts +42 -0
  90. package/Utils/Array.ts +37 -0
  91. package/Utils/CertificateReissue.ts +128 -0
  92. package/Utils/Grok/Grok.ts +461 -0
  93. package/Utils/Grok/GrokPatterns.ts +118 -0
  94. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  95. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  96. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  98. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  100. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  101. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  102. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  103. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  104. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  105. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  106. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  114. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  115. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  116. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  117. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  118. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  119. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  120. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  121. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  122. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  123. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  124. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  125. package/build/dist/Server/Types/Domain.js +16 -0
  126. package/build/dist/Server/Types/Domain.js.map +1 -1
  127. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  128. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  129. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  130. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  131. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  132. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  133. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  134. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  135. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  136. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  137. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  138. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  139. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  140. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  141. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  142. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  143. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  144. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  145. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  146. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  147. package/build/dist/Types/API/Route.js +16 -5
  148. package/build/dist/Types/API/Route.js.map +1 -1
  149. package/build/dist/Types/Date.js +115 -13
  150. package/build/dist/Types/Date.js.map +1 -1
  151. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  152. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  153. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  154. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  155. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  157. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  158. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  159. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  160. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  161. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  162. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  163. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  164. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  165. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  166. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  167. package/build/dist/UI/Utils/Navigation.js +30 -0
  168. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  169. package/build/dist/Utils/Array.js +25 -0
  170. package/build/dist/Utils/Array.js.map +1 -1
  171. package/build/dist/Utils/CertificateReissue.js +97 -0
  172. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  173. package/build/dist/Utils/Grok/Grok.js +324 -0
  174. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  175. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  176. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  178. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  179. package/package.json +1 -1
  180. package/UI/Utils/JsonWebToken.ts +0 -14
  181. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  182. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,880 @@
1
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
2
+ import type { TurnContext } from "botbuilder";
3
+ import type { SpyInstance } from "jest-mock";
4
+ import { FindOperator } from "typeorm";
5
+ import Alert from "../../../../Models/DatabaseModels/Alert";
6
+ import AlertOwnerTeam from "../../../../Models/DatabaseModels/AlertOwnerTeam";
7
+ import Incident from "../../../../Models/DatabaseModels/Incident";
8
+ import IncidentOwnerTeam from "../../../../Models/DatabaseModels/IncidentOwnerTeam";
9
+ import Label from "../../../../Models/DatabaseModels/Label";
10
+ import AlertOwnerTeamService from "../../../../Server/Services/AlertOwnerTeamService";
11
+ import AlertOwnerUserService from "../../../../Server/Services/AlertOwnerUserService";
12
+ import AlertService from "../../../../Server/Services/AlertService";
13
+ import IncidentOwnerTeamService from "../../../../Server/Services/IncidentOwnerTeamService";
14
+ import IncidentOwnerUserService from "../../../../Server/Services/IncidentOwnerUserService";
15
+ import IncidentService from "../../../../Server/Services/IncidentService";
16
+ import ModelPermission from "../../../../Server/Types/Database/Permissions/Index";
17
+ import MicrosoftTeamsAlertActions from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Alert";
18
+ import MicrosoftTeamsActionAuthorization from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization";
19
+ import MicrosoftTeamsIncidentActions from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Incident";
20
+ import {
21
+ MicrosoftTeamsAlertActionType,
22
+ MicrosoftTeamsIncidentActionType,
23
+ } from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
24
+ import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
25
+ import PermissionScope from "../../../../Types/Database/AccessControl/PermissionScope";
26
+ import NotAuthorizedException from "../../../../Types/Exception/NotAuthorizedException";
27
+ import ObjectID from "../../../../Types/ObjectID";
28
+ import Permission, {
29
+ UserGlobalAccessPermission,
30
+ UserPermission,
31
+ UserTenantAccessPermission,
32
+ } from "../../../../Types/Permission";
33
+
34
+ type PermissionInput = {
35
+ permission: Permission;
36
+ isBlockPermission?: boolean;
37
+ labelIds?: Array<ObjectID>;
38
+ scope?: PermissionScope;
39
+ };
40
+
41
+ type IncidentActionTestCase = {
42
+ actionType: MicrosoftTeamsIncidentActionType;
43
+ mutation: "acknowledgeIncident" | "resolveIncident";
44
+ };
45
+
46
+ type AlertActionTestCase = {
47
+ actionType: MicrosoftTeamsAlertActionType;
48
+ mutation: "acknowledgeAlert" | "resolveAlert";
49
+ };
50
+
51
+ type IncidentMutationSpy = SpyInstance<
52
+ typeof IncidentService.acknowledgeIncident
53
+ >;
54
+
55
+ type AlertMutationSpy =
56
+ | SpyInstance<typeof AlertService.acknowledgeAlert>
57
+ | SpyInstance<typeof AlertService.resolveAlert>;
58
+
59
+ const projectId: ObjectID = ObjectID.generate();
60
+ const userId: ObjectID = ObjectID.generate();
61
+
62
+ function createDatabaseProps(
63
+ permissions: Array<PermissionInput>,
64
+ userTeamIds: Array<ObjectID> = [],
65
+ ): DatabaseCommonInteractionProps {
66
+ const userPermissions: Array<UserPermission> = permissions.map(
67
+ (permissionInput: PermissionInput): UserPermission => {
68
+ return {
69
+ _type: "UserPermission",
70
+ permission: permissionInput.permission,
71
+ isBlockPermission: permissionInput.isBlockPermission || false,
72
+ labelIds: permissionInput.labelIds || [],
73
+ scope: permissionInput.scope,
74
+ };
75
+ },
76
+ );
77
+
78
+ const tenantAccessPermission: UserTenantAccessPermission = {
79
+ _type: "UserTenantAccessPermission",
80
+ projectId,
81
+ permissions: userPermissions,
82
+ };
83
+
84
+ const globalAccessPermission: UserGlobalAccessPermission = {
85
+ _type: "UserGlobalAccessPermission",
86
+ globalPermissions: [
87
+ Permission.Public,
88
+ Permission.User,
89
+ Permission.CurrentUser,
90
+ ],
91
+ projectIds: [projectId],
92
+ };
93
+
94
+ return {
95
+ userId,
96
+ tenantId: projectId,
97
+ userTeamIds,
98
+ userGlobalAccessPermission: globalAccessPermission,
99
+ userTenantAccessPermission: {
100
+ [projectId.toString()]: tenantAccessPermission,
101
+ },
102
+ };
103
+ }
104
+
105
+ function createIncident(incidentId: ObjectID): Incident {
106
+ const incident: Incident = new Incident();
107
+ incident.id = incidentId;
108
+ incident.projectId = projectId;
109
+ incident.labels = [];
110
+ return incident;
111
+ }
112
+
113
+ function createAlert(alertId: ObjectID): Alert {
114
+ const alert: Alert = new Alert();
115
+ alert.id = alertId;
116
+ alert.projectId = projectId;
117
+ alert.labels = [];
118
+ return alert;
119
+ }
120
+
121
+ function getRawParameterValues(value: unknown): Array<unknown> {
122
+ const findOperator: FindOperator<unknown> = value as FindOperator<unknown>;
123
+ return Object.values(findOperator.objectLiteralParameters || {});
124
+ }
125
+
126
+ function createTurnContext(): TurnContext {
127
+ return {
128
+ activity: {},
129
+ deleteActivity: jest.fn(async (): Promise<void> => {}),
130
+ sendActivity: jest.fn(async (): Promise<void> => {}),
131
+ } as unknown as TurnContext;
132
+ }
133
+
134
+ afterEach((): void => {
135
+ jest.restoreAllMocks();
136
+ });
137
+
138
+ describe("MicrosoftTeamsActionAuthorization", (): void => {
139
+ test("allows an incident update when the user has an incident edit grant", async (): Promise<void> => {
140
+ const incidentId: ObjectID = ObjectID.generate();
141
+ const incident: Incident = createIncident(incidentId);
142
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
143
+ { permission: Permission.EditProjectIncident },
144
+ { permission: Permission.ReadProjectIncident },
145
+ ]);
146
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
147
+ .spyOn(IncidentService, "findOneBy")
148
+ .mockResolvedValue(incident);
149
+
150
+ await expect(
151
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
152
+ incidentId,
153
+ projectId,
154
+ props: databaseProps,
155
+ }),
156
+ ).resolves.toBeUndefined();
157
+
158
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
159
+ });
160
+
161
+ test("denies an incident update to a viewer with no update grant", async (): Promise<void> => {
162
+ const incidentId: ObjectID = ObjectID.generate();
163
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
164
+ { permission: Permission.Viewer },
165
+ ]);
166
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
167
+ .spyOn(IncidentService, "findOneBy")
168
+ .mockResolvedValue(createIncident(incidentId));
169
+
170
+ await expect(
171
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
172
+ incidentId,
173
+ projectId,
174
+ props: databaseProps,
175
+ }),
176
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
177
+
178
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
179
+ });
180
+
181
+ test("honors an explicit incident update block even when another update grant exists", async (): Promise<void> => {
182
+ const incidentId: ObjectID = ObjectID.generate();
183
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
184
+ { permission: Permission.ProjectMember },
185
+ {
186
+ permission: Permission.EditProjectIncident,
187
+ isBlockPermission: true,
188
+ },
189
+ ]);
190
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
191
+ .spyOn(IncidentService, "findOneBy")
192
+ .mockResolvedValue(createIncident(incidentId));
193
+
194
+ await expect(
195
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
196
+ incidentId,
197
+ projectId,
198
+ props: databaseProps,
199
+ }),
200
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
201
+
202
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
203
+ });
204
+
205
+ test("allows an incident update when its label matches the user's scoped grant", async (): Promise<void> => {
206
+ const incidentId: ObjectID = ObjectID.generate();
207
+ const permittedLabelId: ObjectID = ObjectID.generate();
208
+ const incident: Incident = createIncident(incidentId);
209
+ incident.labels = [new Label(permittedLabelId)];
210
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
211
+ {
212
+ permission: Permission.EditProjectIncident,
213
+ labelIds: [permittedLabelId],
214
+ },
215
+ {
216
+ permission: Permission.ReadProjectIncident,
217
+ labelIds: [permittedLabelId],
218
+ },
219
+ ]);
220
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
221
+ .spyOn(IncidentService, "findOneBy")
222
+ .mockResolvedValue(incident);
223
+
224
+ await expect(
225
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
226
+ incidentId,
227
+ projectId,
228
+ props: databaseProps,
229
+ }),
230
+ ).resolves.toBeUndefined();
231
+
232
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
233
+ });
234
+
235
+ test("honors an incident update block scoped to the incident's label", async (): Promise<void> => {
236
+ const incidentId: ObjectID = ObjectID.generate();
237
+ const blockedLabelId: ObjectID = ObjectID.generate();
238
+ const incident: Incident = createIncident(incidentId);
239
+ incident.labels = [new Label(blockedLabelId)];
240
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
241
+ { permission: Permission.ProjectMember },
242
+ {
243
+ permission: Permission.EditProjectIncident,
244
+ isBlockPermission: true,
245
+ labelIds: [blockedLabelId],
246
+ },
247
+ ]);
248
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
249
+ .spyOn(IncidentService, "findOneBy")
250
+ .mockResolvedValue(incident);
251
+
252
+ await expect(
253
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
254
+ incidentId,
255
+ projectId,
256
+ props: databaseProps,
257
+ }),
258
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
259
+
260
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
261
+ });
262
+
263
+ test("rejects an incident when the exact project and resource query is not permitted", async (): Promise<void> => {
264
+ const incidentId: ObjectID = ObjectID.generate();
265
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
266
+ { permission: Permission.EditProjectIncident },
267
+ { permission: Permission.ReadProjectIncident },
268
+ ]);
269
+ const incident: Incident = createIncident(incidentId);
270
+ incident.isPrivate = true;
271
+ const queryPermissionSpy: SpyInstance<
272
+ typeof ModelPermission.checkUpdateQueryPermissions
273
+ > = jest.spyOn(ModelPermission, "checkUpdateQueryPermissions");
274
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
275
+ .spyOn(IncidentService, "findOneBy")
276
+ .mockResolvedValueOnce(incident)
277
+ .mockResolvedValueOnce(null);
278
+
279
+ await expect(
280
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
281
+ incidentId,
282
+ projectId,
283
+ props: databaseProps,
284
+ }),
285
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
286
+
287
+ expect(queryPermissionSpy).toHaveBeenCalledWith(
288
+ Incident,
289
+ expect.objectContaining({
290
+ _id: incidentId,
291
+ projectId,
292
+ }),
293
+ expect.objectContaining({
294
+ currentIncidentStateId: expect.any(ObjectID),
295
+ }),
296
+ databaseProps,
297
+ );
298
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
299
+ expect(findOneBySpy.mock.calls[0]?.[0].query).toEqual({
300
+ _id: incidentId,
301
+ projectId,
302
+ });
303
+ expect(findOneBySpy.mock.calls[1]?.[0].query).toEqual(
304
+ expect.objectContaining({
305
+ isPrivate: expect.anything(),
306
+ }),
307
+ );
308
+ expect(findOneBySpy.mock.calls[1]?.[0].props).toEqual({ isRoot: true });
309
+ });
310
+
311
+ test("allows an alert update when the user has an alert edit grant", async (): Promise<void> => {
312
+ const alertId: ObjectID = ObjectID.generate();
313
+ const alert: Alert = createAlert(alertId);
314
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
315
+ { permission: Permission.EditAlert },
316
+ { permission: Permission.ReadAlert },
317
+ ]);
318
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
319
+ .spyOn(AlertService, "findOneBy")
320
+ .mockResolvedValue(alert);
321
+
322
+ await expect(
323
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
324
+ alertId,
325
+ projectId,
326
+ props: databaseProps,
327
+ }),
328
+ ).resolves.toBeUndefined();
329
+
330
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
331
+ });
332
+
333
+ test("denies an alert update to a viewer with no update grant", async (): Promise<void> => {
334
+ const alertId: ObjectID = ObjectID.generate();
335
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
336
+ { permission: Permission.Viewer },
337
+ ]);
338
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
339
+ .spyOn(AlertService, "findOneBy")
340
+ .mockResolvedValue(createAlert(alertId));
341
+
342
+ await expect(
343
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
344
+ alertId,
345
+ projectId,
346
+ props: databaseProps,
347
+ }),
348
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
349
+
350
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
351
+ });
352
+
353
+ test("honors an explicit alert update block even when another update grant exists", async (): Promise<void> => {
354
+ const alertId: ObjectID = ObjectID.generate();
355
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
356
+ { permission: Permission.ProjectMember },
357
+ {
358
+ permission: Permission.EditAlert,
359
+ isBlockPermission: true,
360
+ },
361
+ ]);
362
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
363
+ .spyOn(AlertService, "findOneBy")
364
+ .mockResolvedValue(createAlert(alertId));
365
+
366
+ await expect(
367
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
368
+ alertId,
369
+ projectId,
370
+ props: databaseProps,
371
+ }),
372
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
373
+
374
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
375
+ });
376
+
377
+ test("denies an alert update when its label is outside the user's scoped grant", async (): Promise<void> => {
378
+ const alertId: ObjectID = ObjectID.generate();
379
+ const alert: Alert = createAlert(alertId);
380
+ alert.labels = [new Label(ObjectID.generate())];
381
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
382
+ {
383
+ permission: Permission.EditAlert,
384
+ labelIds: [ObjectID.generate()],
385
+ },
386
+ ]);
387
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
388
+ .spyOn(AlertService, "findOneBy")
389
+ .mockResolvedValue(alert);
390
+
391
+ await expect(
392
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
393
+ alertId,
394
+ projectId,
395
+ props: databaseProps,
396
+ }),
397
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
398
+
399
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
400
+ });
401
+
402
+ test("rejects an alert when the exact project and resource query is not permitted", async (): Promise<void> => {
403
+ const alertId: ObjectID = ObjectID.generate();
404
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
405
+ { permission: Permission.EditAlert },
406
+ { permission: Permission.ReadAlert },
407
+ ]);
408
+ const alert: Alert = createAlert(alertId);
409
+ alert.isPrivate = true;
410
+ const queryPermissionSpy: SpyInstance<
411
+ typeof ModelPermission.checkUpdateQueryPermissions
412
+ > = jest.spyOn(ModelPermission, "checkUpdateQueryPermissions");
413
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
414
+ .spyOn(AlertService, "findOneBy")
415
+ .mockResolvedValueOnce(alert)
416
+ .mockResolvedValueOnce(null);
417
+
418
+ await expect(
419
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
420
+ alertId,
421
+ projectId,
422
+ props: databaseProps,
423
+ }),
424
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
425
+
426
+ expect(queryPermissionSpy).toHaveBeenCalledWith(
427
+ Alert,
428
+ expect.objectContaining({
429
+ _id: alertId,
430
+ projectId,
431
+ }),
432
+ expect.objectContaining({
433
+ currentAlertStateId: expect.any(ObjectID),
434
+ }),
435
+ databaseProps,
436
+ );
437
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
438
+ expect(findOneBySpy.mock.calls[0]?.[0].query).toEqual({
439
+ _id: alertId,
440
+ projectId,
441
+ });
442
+ expect(findOneBySpy.mock.calls[1]?.[0].query).toEqual(
443
+ expect.objectContaining({
444
+ isPrivate: expect.anything(),
445
+ }),
446
+ );
447
+ expect(findOneBySpy.mock.calls[1]?.[0].props).toEqual({ isRoot: true });
448
+ });
449
+
450
+ test("allows an incident owned by one of the user's accepted teams", async (): Promise<void> => {
451
+ const incidentId: ObjectID = ObjectID.generate();
452
+ const teamId: ObjectID = ObjectID.generate();
453
+ const incident: Incident = createIncident(incidentId);
454
+ incident.isPrivate = true;
455
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
456
+ [
457
+ {
458
+ permission: Permission.EditProjectIncident,
459
+ scope: PermissionScope.Owned,
460
+ },
461
+ { permission: Permission.ReadProjectIncident },
462
+ ],
463
+ [teamId],
464
+ );
465
+ const ownerTeam: IncidentOwnerTeam = new IncidentOwnerTeam();
466
+ ownerTeam.incidentId = incidentId;
467
+ ownerTeam.projectId = projectId;
468
+ ownerTeam.teamId = teamId;
469
+
470
+ const ownerUserSpy: SpyInstance<typeof IncidentOwnerUserService.findBy> =
471
+ jest.spyOn(IncidentOwnerUserService, "findBy").mockResolvedValue([]);
472
+ const ownerTeamSpy: SpyInstance<typeof IncidentOwnerTeamService.findBy> =
473
+ jest
474
+ .spyOn(IncidentOwnerTeamService, "findBy")
475
+ .mockResolvedValue([ownerTeam]);
476
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
477
+ .spyOn(IncidentService, "findOneBy")
478
+ .mockResolvedValue(incident);
479
+
480
+ await expect(
481
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
482
+ incidentId,
483
+ projectId,
484
+ props: databaseProps,
485
+ }),
486
+ ).resolves.toBeUndefined();
487
+
488
+ expect(ownerUserSpy).toHaveBeenCalledWith(
489
+ expect.objectContaining({
490
+ query: {
491
+ userId,
492
+ projectId,
493
+ },
494
+ }),
495
+ );
496
+ expect(ownerTeamSpy).toHaveBeenCalledTimes(1);
497
+ expect(ownerTeamSpy.mock.calls[0]?.[0].query.projectId).toEqual(projectId);
498
+ expect(
499
+ getRawParameterValues(ownerTeamSpy.mock.calls[0]?.[0].query.teamId),
500
+ ).toEqual([[teamId.toString()]]);
501
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
502
+ expect(findOneBySpy.mock.calls[1]?.[0].query).toEqual(
503
+ expect.objectContaining({
504
+ isPrivate: expect.anything(),
505
+ }),
506
+ );
507
+ });
508
+
509
+ test("rejects an owned-scoped incident when neither the user nor an accepted team owns it", async (): Promise<void> => {
510
+ const incidentId: ObjectID = ObjectID.generate();
511
+ const teamId: ObjectID = ObjectID.generate();
512
+ const incident: Incident = createIncident(incidentId);
513
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
514
+ [
515
+ {
516
+ permission: Permission.EditProjectIncident,
517
+ scope: PermissionScope.Owned,
518
+ },
519
+ { permission: Permission.ReadProjectIncident },
520
+ ],
521
+ [teamId],
522
+ );
523
+
524
+ jest.spyOn(IncidentOwnerUserService, "findBy").mockResolvedValue([]);
525
+ const ownerTeamSpy: SpyInstance<typeof IncidentOwnerTeamService.findBy> =
526
+ jest.spyOn(IncidentOwnerTeamService, "findBy").mockResolvedValue([]);
527
+ const findOneBySpy: SpyInstance<typeof IncidentService.findOneBy> = jest
528
+ .spyOn(IncidentService, "findOneBy")
529
+ .mockResolvedValueOnce(incident)
530
+ .mockResolvedValueOnce(null);
531
+
532
+ await expect(
533
+ MicrosoftTeamsActionAuthorization.assertCanUpdateIncident({
534
+ incidentId,
535
+ projectId,
536
+ props: databaseProps,
537
+ }),
538
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
539
+
540
+ expect(ownerTeamSpy).toHaveBeenCalledTimes(1);
541
+ expect(
542
+ getRawParameterValues(ownerTeamSpy.mock.calls[0]?.[0].query.teamId),
543
+ ).toEqual([[teamId.toString()]]);
544
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
545
+ });
546
+
547
+ test("allows an alert owned by one of the user's accepted teams", async (): Promise<void> => {
548
+ const alertId: ObjectID = ObjectID.generate();
549
+ const teamId: ObjectID = ObjectID.generate();
550
+ const alert: Alert = createAlert(alertId);
551
+ alert.isPrivate = true;
552
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
553
+ [
554
+ {
555
+ permission: Permission.EditAlert,
556
+ scope: PermissionScope.Owned,
557
+ },
558
+ { permission: Permission.ReadAlert },
559
+ ],
560
+ [teamId],
561
+ );
562
+ const ownerTeam: AlertOwnerTeam = new AlertOwnerTeam();
563
+ ownerTeam.alertId = alertId;
564
+ ownerTeam.projectId = projectId;
565
+ ownerTeam.teamId = teamId;
566
+
567
+ const ownerUserSpy: SpyInstance<typeof AlertOwnerUserService.findBy> = jest
568
+ .spyOn(AlertOwnerUserService, "findBy")
569
+ .mockResolvedValue([]);
570
+ const ownerTeamSpy: SpyInstance<typeof AlertOwnerTeamService.findBy> = jest
571
+ .spyOn(AlertOwnerTeamService, "findBy")
572
+ .mockResolvedValue([ownerTeam]);
573
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
574
+ .spyOn(AlertService, "findOneBy")
575
+ .mockResolvedValue(alert);
576
+
577
+ await expect(
578
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
579
+ alertId,
580
+ projectId,
581
+ props: databaseProps,
582
+ }),
583
+ ).resolves.toBeUndefined();
584
+
585
+ expect(ownerUserSpy).toHaveBeenCalledWith(
586
+ expect.objectContaining({
587
+ query: {
588
+ userId,
589
+ projectId,
590
+ },
591
+ }),
592
+ );
593
+ expect(ownerTeamSpy).toHaveBeenCalledTimes(1);
594
+ expect(ownerTeamSpy.mock.calls[0]?.[0].query.projectId).toEqual(projectId);
595
+ expect(
596
+ getRawParameterValues(ownerTeamSpy.mock.calls[0]?.[0].query.teamId),
597
+ ).toEqual([[teamId.toString()]]);
598
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
599
+ expect(findOneBySpy.mock.calls[1]?.[0].query).toEqual(
600
+ expect.objectContaining({
601
+ isPrivate: expect.anything(),
602
+ }),
603
+ );
604
+ });
605
+
606
+ test("rejects an owned-scoped alert when neither the user nor an accepted team owns it", async (): Promise<void> => {
607
+ const alertId: ObjectID = ObjectID.generate();
608
+ const teamId: ObjectID = ObjectID.generate();
609
+ const alert: Alert = createAlert(alertId);
610
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
611
+ [
612
+ {
613
+ permission: Permission.EditAlert,
614
+ scope: PermissionScope.Owned,
615
+ },
616
+ { permission: Permission.ReadAlert },
617
+ ],
618
+ [teamId],
619
+ );
620
+
621
+ jest.spyOn(AlertOwnerUserService, "findBy").mockResolvedValue([]);
622
+ const ownerTeamSpy: SpyInstance<typeof AlertOwnerTeamService.findBy> = jest
623
+ .spyOn(AlertOwnerTeamService, "findBy")
624
+ .mockResolvedValue([]);
625
+ const findOneBySpy: SpyInstance<typeof AlertService.findOneBy> = jest
626
+ .spyOn(AlertService, "findOneBy")
627
+ .mockResolvedValueOnce(alert)
628
+ .mockResolvedValueOnce(null);
629
+
630
+ await expect(
631
+ MicrosoftTeamsActionAuthorization.assertCanUpdateAlert({
632
+ alertId,
633
+ projectId,
634
+ props: databaseProps,
635
+ }),
636
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
637
+
638
+ expect(ownerTeamSpy).toHaveBeenCalledTimes(1);
639
+ expect(
640
+ getRawParameterValues(ownerTeamSpy.mock.calls[0]?.[0].query.teamId),
641
+ ).toEqual([[teamId.toString()]]);
642
+ expect(findOneBySpy).toHaveBeenCalledTimes(2);
643
+ });
644
+ });
645
+
646
+ describe("MicrosoftTeamsIncidentActions authorization", (): void => {
647
+ const incidentActions: ReadonlyArray<IncidentActionTestCase> = [
648
+ {
649
+ actionType: MicrosoftTeamsIncidentActionType.AckIncident,
650
+ mutation: "acknowledgeIncident",
651
+ },
652
+ {
653
+ actionType: MicrosoftTeamsIncidentActionType.ResolveIncident,
654
+ mutation: "resolveIncident",
655
+ },
656
+ ];
657
+
658
+ test.each(incidentActions)(
659
+ "$actionType does not mutate the incident when authorization is denied",
660
+ async ({ actionType, mutation }: IncidentActionTestCase): Promise<void> => {
661
+ const incidentId: ObjectID = ObjectID.generate();
662
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
663
+ [{ permission: Permission.Viewer }],
664
+ );
665
+ const turnContext: TurnContext = createTurnContext();
666
+ const authorizationError: NotAuthorizedException =
667
+ new NotAuthorizedException("Not authorized");
668
+ jest
669
+ .spyOn(MicrosoftTeamsActionAuthorization, "assertCanUpdateIncident")
670
+ .mockRejectedValue(authorizationError);
671
+ const mutationSpy: IncidentMutationSpy = jest
672
+ .spyOn(IncidentService, mutation)
673
+ .mockResolvedValue(createIncident(incidentId));
674
+
675
+ await expect(
676
+ MicrosoftTeamsIncidentActions.handleBotIncidentAction({
677
+ actionType,
678
+ actionValue: incidentId.toString(),
679
+ value: {},
680
+ projectId,
681
+ oneUptimeUserId: userId,
682
+ databaseProps,
683
+ turnContext,
684
+ }),
685
+ ).rejects.toBe(authorizationError);
686
+
687
+ expect(mutationSpy).not.toHaveBeenCalled();
688
+ },
689
+ );
690
+
691
+ test.each(incidentActions)(
692
+ "$actionType mutates the incident exactly once after authorization succeeds",
693
+ async ({ actionType, mutation }: IncidentActionTestCase): Promise<void> => {
694
+ const incidentId: ObjectID = ObjectID.generate();
695
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
696
+ [{ permission: Permission.EditProjectIncident }],
697
+ );
698
+ const turnContext: TurnContext = createTurnContext();
699
+ const authorizationSpy: SpyInstance<
700
+ typeof MicrosoftTeamsActionAuthorization.assertCanUpdateIncident
701
+ > = jest
702
+ .spyOn(MicrosoftTeamsActionAuthorization, "assertCanUpdateIncident")
703
+ .mockResolvedValue();
704
+ const mutationSpy: IncidentMutationSpy = jest
705
+ .spyOn(IncidentService, mutation)
706
+ .mockResolvedValue(createIncident(incidentId));
707
+
708
+ await MicrosoftTeamsIncidentActions.handleBotIncidentAction({
709
+ actionType,
710
+ actionValue: incidentId.toString(),
711
+ value: {},
712
+ projectId,
713
+ oneUptimeUserId: userId,
714
+ databaseProps,
715
+ turnContext,
716
+ });
717
+
718
+ expect(authorizationSpy).toHaveBeenCalledTimes(1);
719
+ expect(authorizationSpy).toHaveBeenCalledWith({
720
+ incidentId,
721
+ projectId,
722
+ props: databaseProps,
723
+ });
724
+ expect(mutationSpy).toHaveBeenCalledTimes(1);
725
+ expect(mutationSpy).toHaveBeenCalledWith(incidentId, userId);
726
+ },
727
+ );
728
+
729
+ test("passes the authenticated user's database props when changing incident state", async (): Promise<void> => {
730
+ const incidentId: ObjectID = ObjectID.generate();
731
+ const incidentStateId: ObjectID = ObjectID.generate();
732
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
733
+ { permission: Permission.EditProjectIncident },
734
+ ]);
735
+ const turnContext: TurnContext = createTurnContext();
736
+ const updateSpy: SpyInstance<typeof IncidentService.updateOneById> = jest
737
+ .spyOn(IncidentService, "updateOneById")
738
+ .mockResolvedValue(1);
739
+
740
+ await MicrosoftTeamsIncidentActions.handleBotIncidentAction({
741
+ actionType: MicrosoftTeamsIncidentActionType.SubmitChangeIncidentState,
742
+ actionValue: incidentId.toString(),
743
+ value: {
744
+ incidentState: incidentStateId.toString(),
745
+ },
746
+ projectId,
747
+ oneUptimeUserId: userId,
748
+ databaseProps,
749
+ turnContext,
750
+ });
751
+
752
+ expect(updateSpy).toHaveBeenCalledTimes(1);
753
+ const updateArgs: Parameters<typeof IncidentService.updateOneById>[0] =
754
+ updateSpy.mock.calls[0]![0];
755
+ expect(updateArgs.id).toEqual(incidentId);
756
+ expect(updateArgs.data.currentIncidentStateId).toEqual(incidentStateId);
757
+ expect(updateArgs.props).toBe(databaseProps);
758
+ });
759
+ });
760
+
761
+ describe("MicrosoftTeamsAlertActions authorization", (): void => {
762
+ const alertActions: ReadonlyArray<AlertActionTestCase> = [
763
+ {
764
+ actionType: MicrosoftTeamsAlertActionType.AckAlert,
765
+ mutation: "acknowledgeAlert",
766
+ },
767
+ {
768
+ actionType: MicrosoftTeamsAlertActionType.ResolveAlert,
769
+ mutation: "resolveAlert",
770
+ },
771
+ ];
772
+
773
+ test.each(alertActions)(
774
+ "$actionType does not mutate the alert when authorization is denied",
775
+ async ({ actionType, mutation }: AlertActionTestCase): Promise<void> => {
776
+ const alertId: ObjectID = ObjectID.generate();
777
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
778
+ [{ permission: Permission.Viewer }],
779
+ );
780
+ const turnContext: TurnContext = createTurnContext();
781
+ const authorizationError: NotAuthorizedException =
782
+ new NotAuthorizedException("Not authorized");
783
+ jest
784
+ .spyOn(MicrosoftTeamsActionAuthorization, "assertCanUpdateAlert")
785
+ .mockRejectedValue(authorizationError);
786
+ const mutationSpy: AlertMutationSpy =
787
+ mutation === "acknowledgeAlert"
788
+ ? jest.spyOn(AlertService, mutation).mockResolvedValue()
789
+ : jest
790
+ .spyOn(AlertService, mutation)
791
+ .mockResolvedValue(createAlert(alertId));
792
+
793
+ await expect(
794
+ MicrosoftTeamsAlertActions.handleBotAlertAction({
795
+ actionType,
796
+ actionValue: alertId.toString(),
797
+ value: {},
798
+ projectId,
799
+ oneUptimeUserId: userId,
800
+ databaseProps,
801
+ turnContext,
802
+ }),
803
+ ).rejects.toBe(authorizationError);
804
+
805
+ expect(mutationSpy).not.toHaveBeenCalled();
806
+ },
807
+ );
808
+
809
+ test.each(alertActions)(
810
+ "$actionType mutates the alert exactly once after authorization succeeds",
811
+ async ({ actionType, mutation }: AlertActionTestCase): Promise<void> => {
812
+ const alertId: ObjectID = ObjectID.generate();
813
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps(
814
+ [{ permission: Permission.EditAlert }],
815
+ );
816
+ const turnContext: TurnContext = createTurnContext();
817
+ const authorizationSpy: SpyInstance<
818
+ typeof MicrosoftTeamsActionAuthorization.assertCanUpdateAlert
819
+ > = jest
820
+ .spyOn(MicrosoftTeamsActionAuthorization, "assertCanUpdateAlert")
821
+ .mockResolvedValue();
822
+ const mutationSpy: AlertMutationSpy =
823
+ mutation === "acknowledgeAlert"
824
+ ? jest.spyOn(AlertService, mutation).mockResolvedValue()
825
+ : jest
826
+ .spyOn(AlertService, mutation)
827
+ .mockResolvedValue(createAlert(alertId));
828
+
829
+ await MicrosoftTeamsAlertActions.handleBotAlertAction({
830
+ actionType,
831
+ actionValue: alertId.toString(),
832
+ value: {},
833
+ projectId,
834
+ oneUptimeUserId: userId,
835
+ databaseProps,
836
+ turnContext,
837
+ });
838
+
839
+ expect(authorizationSpy).toHaveBeenCalledTimes(1);
840
+ expect(authorizationSpy).toHaveBeenCalledWith({
841
+ alertId,
842
+ projectId,
843
+ props: databaseProps,
844
+ });
845
+ expect(mutationSpy).toHaveBeenCalledTimes(1);
846
+ expect(mutationSpy).toHaveBeenCalledWith(alertId, userId);
847
+ },
848
+ );
849
+
850
+ test("passes the authenticated user's database props when changing alert state", async (): Promise<void> => {
851
+ const alertId: ObjectID = ObjectID.generate();
852
+ const alertStateId: ObjectID = ObjectID.generate();
853
+ const databaseProps: DatabaseCommonInteractionProps = createDatabaseProps([
854
+ { permission: Permission.EditAlert },
855
+ ]);
856
+ const turnContext: TurnContext = createTurnContext();
857
+ const updateSpy: SpyInstance<typeof AlertService.updateOneById> = jest
858
+ .spyOn(AlertService, "updateOneById")
859
+ .mockResolvedValue(1);
860
+
861
+ await MicrosoftTeamsAlertActions.handleBotAlertAction({
862
+ actionType: MicrosoftTeamsAlertActionType.SubmitChangeAlertState,
863
+ actionValue: alertId.toString(),
864
+ value: {
865
+ alertState: alertStateId.toString(),
866
+ },
867
+ projectId,
868
+ oneUptimeUserId: userId,
869
+ databaseProps,
870
+ turnContext,
871
+ });
872
+
873
+ expect(updateSpy).toHaveBeenCalledTimes(1);
874
+ const updateArgs: Parameters<typeof AlertService.updateOneById>[0] =
875
+ updateSpy.mock.calls[0]![0];
876
+ expect(updateArgs.id).toEqual(alertId);
877
+ expect(updateArgs.data.currentAlertStateId).toEqual(alertStateId);
878
+ expect(updateArgs.props).toBe(databaseProps);
879
+ });
880
+ });