@oneuptime/common 14.0.6 → 14.0.7
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/InventoryItem.ts +23 -12
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentService.ts +134 -44
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +96 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,2189 @@
|
|
|
1
|
+
import AlertService from "../../../Server/Services/AlertService";
|
|
2
|
+
import AlertStateService from "../../../Server/Services/AlertStateService";
|
|
3
|
+
import AutoRemediationRuleEngineService from "../../../Server/Services/AutoRemediationRuleEngineService";
|
|
4
|
+
import CustomFieldMappingService from "../../../Server/Services/CustomFieldMappingService";
|
|
5
|
+
import IncidentAlertService, {
|
|
6
|
+
AcknowledgeDeclaredAlertsResult,
|
|
7
|
+
LinkAlertsToIncidentResult,
|
|
8
|
+
} from "../../../Server/Services/IncidentAlertService";
|
|
9
|
+
import IncidentGroupingEngineService from "../../../Server/Services/IncidentGroupingEngineService";
|
|
10
|
+
import IncidentLabelRuleEngineService from "../../../Server/Services/IncidentLabelRuleEngineService";
|
|
11
|
+
import IncidentOnCallRuleEngineService from "../../../Server/Services/IncidentOnCallRuleEngineService";
|
|
12
|
+
import IncidentOwnerRuleEngineService from "../../../Server/Services/IncidentOwnerRuleEngineService";
|
|
13
|
+
import IncidentPrivacyRuleEngineService from "../../../Server/Services/IncidentPrivacyRuleEngineService";
|
|
14
|
+
import IncidentService from "../../../Server/Services/IncidentService";
|
|
15
|
+
import IncidentSlaService from "../../../Server/Services/IncidentSlaService";
|
|
16
|
+
import IncidentStateService from "../../../Server/Services/IncidentStateService";
|
|
17
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
18
|
+
import RunbookRuleEngineService from "../../../Server/Services/RunbookRuleEngineService";
|
|
19
|
+
import UserService from "../../../Server/Services/UserService";
|
|
20
|
+
import { OnCreate } from "../../../Server/Types/Database/Hooks";
|
|
21
|
+
import AIIncidentInvestigationRunner from "../../../Server/Utils/AI/SRE/IncidentInvestigationRunner";
|
|
22
|
+
import AlertStateChangeAuthorization from "../../../Server/Utils/Alert/AlertStateChangeAuthorization";
|
|
23
|
+
import ProjectScopedReferenceValidator from "../../../Server/Utils/Database/ProjectScopedReferenceValidator";
|
|
24
|
+
import logger from "../../../Server/Utils/Logger";
|
|
25
|
+
import ProductAnalytics from "../../../Server/Utils/ProductAnalytics";
|
|
26
|
+
import SloRecordReferenceValidator from "../../../Server/Utils/Slo/SloRecordReferenceValidator";
|
|
27
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
28
|
+
import AlertState from "../../../Models/DatabaseModels/AlertState";
|
|
29
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
30
|
+
import IncidentState from "../../../Models/DatabaseModels/IncidentState";
|
|
31
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
32
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
33
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
34
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
35
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
36
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
37
|
+
import {
|
|
38
|
+
INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY,
|
|
39
|
+
INCIDENT_ALERT_IDS_TO_LINK_KEY,
|
|
40
|
+
} from "../../../Types/Incident/IncidentAlertLink";
|
|
41
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
42
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
43
|
+
import Permission, {
|
|
44
|
+
UserGlobalAccessPermission,
|
|
45
|
+
UserTenantAccessPermission,
|
|
46
|
+
} from "../../../Types/Permission";
|
|
47
|
+
import UserType from "../../../Types/UserType";
|
|
48
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
49
|
+
import { FindOperator } from "typeorm";
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* Declaring an incident from alerts and acknowledging those alerts in the
|
|
53
|
+
* same request (miscDataProps.acknowledgeAlertsToLink), at the two places it
|
|
54
|
+
* hooks into IncidentService:
|
|
55
|
+
*
|
|
56
|
+
* - onBeforeCreate validates the alert ids first and then the request to
|
|
57
|
+
* acknowledge them (with the validated ids), both BEFORE the incident
|
|
58
|
+
* number is taken, so an impossible request neither burns a number nor
|
|
59
|
+
* leaves an incident behind whose alerts keep paging. The project's
|
|
60
|
+
* Acknowledged alert state is carried forward together with the alerts
|
|
61
|
+
* that are not acknowledged yet - the only ones the caller is checked
|
|
62
|
+
* for, so an alert that is already acknowledged or resolved never refuses
|
|
63
|
+
* the declaration;
|
|
64
|
+
* - onCreateSuccess acknowledges exactly those alerts once the links have
|
|
65
|
+
* been written, credited to the declaring user, without waiting for it and
|
|
66
|
+
* without ever failing the incident because of it.
|
|
67
|
+
*
|
|
68
|
+
* Everything around those hooks is stubbed except the two validators (and,
|
|
69
|
+
* where a test says so, the real AlertStateChangeAuthorization); the
|
|
70
|
+
* acknowledgement itself
|
|
71
|
+
* (IncidentAlertService.acknowledgeAlertsDeclaredWithIncident) has its own
|
|
72
|
+
* tests.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
76
|
+
"0194c3a9-0000-4000-8000-000000000001",
|
|
77
|
+
);
|
|
78
|
+
const INCIDENT_ID: ObjectID = new ObjectID(
|
|
79
|
+
"0194c3a9-0000-4000-8000-0000000000a1",
|
|
80
|
+
);
|
|
81
|
+
const ALERT_ID: string = "0194c3a9-0000-4000-8000-0000000000b1";
|
|
82
|
+
const ALERT_ID_2: string = "0194c3a9-0000-4000-8000-0000000000b2";
|
|
83
|
+
const ALERT_ID_3: string = "0194c3a9-0000-4000-8000-0000000000b3";
|
|
84
|
+
const ALERT_ID_4: string = "0194c3a9-0000-4000-8000-0000000000b4";
|
|
85
|
+
const USER_ID: ObjectID = new ObjectID("0194c3a9-0000-4000-8000-0000000000c1");
|
|
86
|
+
const OTHER_USER_ID: ObjectID = new ObjectID(
|
|
87
|
+
"0194c3a9-0000-4000-8000-0000000000c2",
|
|
88
|
+
);
|
|
89
|
+
const CREATED_STATE_ID: string = "0194c3a9-0000-4000-8000-0000000000d1";
|
|
90
|
+
const ACKNOWLEDGED_ALERT_STATE_ID: string =
|
|
91
|
+
"0194c3a9-0000-4000-8000-0000000000f2";
|
|
92
|
+
|
|
93
|
+
// The project's alert states, by order: what "not acknowledged yet" means.
|
|
94
|
+
const CREATED_ALERT_STATE_ORDER: number = 1;
|
|
95
|
+
const ACKNOWLEDGED_ALERT_STATE_ORDER: number = 2;
|
|
96
|
+
const RESOLVED_ALERT_STATE_ORDER: number = 3;
|
|
97
|
+
|
|
98
|
+
const SEVERITY_ID: ObjectID = new ObjectID(
|
|
99
|
+
"0194c3a9-0000-4000-8000-0000000000e1",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const NO_ALERTS_MESSAGE: string =
|
|
103
|
+
"acknowledgeAlertsToLink only applies when the incident is declared from alerts: send the alert ids in alertIdsToLink.";
|
|
104
|
+
const NOT_A_BOOLEAN_MESSAGE: string =
|
|
105
|
+
"acknowledgeAlertsToLink must be true or false.";
|
|
106
|
+
const NO_ACKNOWLEDGED_STATE_MESSAGE: string =
|
|
107
|
+
"This project has no Acknowledged alert state, so the alerts cannot be acknowledged. Declare the incident without acknowledging them, or add an Acknowledged state in the alert settings.";
|
|
108
|
+
const MAY_NOT_ACKNOWLEDGE_MESSAGE: string =
|
|
109
|
+
"You do not have permission to acknowledge one or more of these alerts. Declare the incident without acknowledging them, or ask a project admin for permission.";
|
|
110
|
+
const ACKNOWLEDGE_FAILED_LOG_PREFIX: string =
|
|
111
|
+
"Acknowledging the alerts an incident was declared from failed in IncidentService.onCreateSuccess: ";
|
|
112
|
+
const MAY_NOT_CHANGE_STATE_MESSAGE: string =
|
|
113
|
+
"You do not have permission to change the state of one or more of these alerts.";
|
|
114
|
+
|
|
115
|
+
// What onBeforeCreate hands to onCreateSuccess when declaring from alerts.
|
|
116
|
+
type CarriedForward = {
|
|
117
|
+
alertIdsToLink: Array<ObjectID>;
|
|
118
|
+
acknowledgedAlertStateId: ObjectID | null;
|
|
119
|
+
alertIdsToAcknowledge: Array<ObjectID>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type AuthorizeArgs = {
|
|
123
|
+
projectId: ObjectID;
|
|
124
|
+
alertIds: Array<ObjectID>;
|
|
125
|
+
props: DatabaseCommonInteractionProps;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type FindAlertsArgs = {
|
|
129
|
+
query: Record<string, unknown>;
|
|
130
|
+
select: Record<string, unknown>;
|
|
131
|
+
limit: number;
|
|
132
|
+
skip: number;
|
|
133
|
+
props: DatabaseCommonInteractionProps;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type ValidateAcknowledgeArgs = {
|
|
137
|
+
projectId: ObjectID | undefined;
|
|
138
|
+
acknowledgeAlerts: unknown;
|
|
139
|
+
alertIds: Array<ObjectID>;
|
|
140
|
+
props: DatabaseCommonInteractionProps;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type AcknowledgeArgs = {
|
|
144
|
+
projectId: ObjectID;
|
|
145
|
+
incidentId: ObjectID;
|
|
146
|
+
alertIds: Array<ObjectID>;
|
|
147
|
+
linkedAlertIds: Array<ObjectID>;
|
|
148
|
+
acknowledgedByUserId: ObjectID | undefined;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type LinkArgs = {
|
|
152
|
+
alertIds: Array<ObjectID>;
|
|
153
|
+
createdByUserId: ObjectID | undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type HookFunction = (...args: Array<unknown>) => Promise<unknown>;
|
|
157
|
+
|
|
158
|
+
function callHook<T>(
|
|
159
|
+
service: unknown,
|
|
160
|
+
name: string,
|
|
161
|
+
...args: Array<unknown>
|
|
162
|
+
): Promise<T> {
|
|
163
|
+
const hooks: Record<string, HookFunction> = service as Record<
|
|
164
|
+
string,
|
|
165
|
+
HookFunction
|
|
166
|
+
>;
|
|
167
|
+
return hooks[name]!.apply(service, args) as Promise<T>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function userProps(permission: Permission): DatabaseCommonInteractionProps {
|
|
171
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
172
|
+
projectId: PROJECT_ID,
|
|
173
|
+
_type: "UserTenantAccessPermission",
|
|
174
|
+
permissions: [
|
|
175
|
+
{
|
|
176
|
+
_type: "UserPermission",
|
|
177
|
+
permission: permission,
|
|
178
|
+
labelIds: [],
|
|
179
|
+
isBlockPermission: false,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
tenantId: PROJECT_ID,
|
|
186
|
+
userId: USER_ID,
|
|
187
|
+
userType: UserType.User,
|
|
188
|
+
userTenantAccessPermission: {
|
|
189
|
+
[PROJECT_ID.toString()]: tenantPermission,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// An API key: a project tenant and no user.
|
|
195
|
+
function apiKeyProps(): DatabaseCommonInteractionProps {
|
|
196
|
+
const props: DatabaseCommonInteractionProps = userProps(
|
|
197
|
+
Permission.ProjectMember,
|
|
198
|
+
);
|
|
199
|
+
delete props.userId;
|
|
200
|
+
props.userType = UserType.API;
|
|
201
|
+
return props;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* A responder who can read every alert (Viewer) but may only change the
|
|
206
|
+
* alerts that carry one label (AlertMember for that label) - they may link
|
|
207
|
+
* any alert to an incident, and change the state of only some.
|
|
208
|
+
*/
|
|
209
|
+
function labelScopedProps(label: Label): DatabaseCommonInteractionProps {
|
|
210
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
211
|
+
projectId: PROJECT_ID,
|
|
212
|
+
_type: "UserTenantAccessPermission",
|
|
213
|
+
permissions: [
|
|
214
|
+
{
|
|
215
|
+
_type: "UserPermission",
|
|
216
|
+
permission: Permission.Viewer,
|
|
217
|
+
labelIds: [],
|
|
218
|
+
isBlockPermission: false,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
_type: "UserPermission",
|
|
222
|
+
permission: Permission.AlertMember,
|
|
223
|
+
labelIds: [label.id!],
|
|
224
|
+
isBlockPermission: false,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const globalPermission: UserGlobalAccessPermission = {
|
|
230
|
+
_type: "UserGlobalAccessPermission",
|
|
231
|
+
globalPermissions: [
|
|
232
|
+
Permission.Public,
|
|
233
|
+
Permission.User,
|
|
234
|
+
Permission.CurrentUser,
|
|
235
|
+
],
|
|
236
|
+
projectIds: [PROJECT_ID],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
tenantId: PROJECT_ID,
|
|
241
|
+
userId: USER_ID,
|
|
242
|
+
userType: UserType.User,
|
|
243
|
+
userTeamIds: [],
|
|
244
|
+
userGlobalAccessPermission: globalPermission,
|
|
245
|
+
userTenantAccessPermission: {
|
|
246
|
+
[PROJECT_ID.toString()]: tenantPermission,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createLabel(name: string): Label {
|
|
252
|
+
const label: Label = new Label(ObjectID.generate());
|
|
253
|
+
label.name = name;
|
|
254
|
+
return label;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function settle(): Promise<void> {
|
|
258
|
+
// Let fire-and-forget chains run to completion while the stubs are live.
|
|
259
|
+
for (let i: number = 0; i < 5; i++) {
|
|
260
|
+
await new Promise<void>((resolve: () => void) => {
|
|
261
|
+
setTimeout(resolve, 0);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface Deferred<T> {
|
|
267
|
+
promise: Promise<T>;
|
|
268
|
+
resolve: (value: T) => void;
|
|
269
|
+
reject: (error: Error) => void;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function deferred<T>(): Deferred<T> {
|
|
273
|
+
let resolve: (value: T) => void = (): void => {
|
|
274
|
+
// replaced below
|
|
275
|
+
};
|
|
276
|
+
let reject: (error: Error) => void = (): void => {
|
|
277
|
+
// replaced below
|
|
278
|
+
};
|
|
279
|
+
const promise: Promise<T> = new Promise<T>(
|
|
280
|
+
(res: (value: T) => void, rej: (error: Error) => void) => {
|
|
281
|
+
resolve = res;
|
|
282
|
+
reject = rej;
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
return { promise: promise, resolve: resolve, reject: reject };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function ids(values: Array<ObjectID>): Array<string> {
|
|
289
|
+
return values.map((value: ObjectID): string => {
|
|
290
|
+
return value.toString();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function emptyAcknowledgeResult(): AcknowledgeDeclaredAlertsResult {
|
|
295
|
+
return {
|
|
296
|
+
acknowledgedAlertIds: [],
|
|
297
|
+
alreadyAcknowledgedAlertIds: [],
|
|
298
|
+
leftToLinkedAlertSyncAlertIds: [],
|
|
299
|
+
failed: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function acknowledgedAlertState(): AlertState {
|
|
304
|
+
const state: AlertState = new AlertState();
|
|
305
|
+
state._id = ACKNOWLEDGED_ALERT_STATE_ID;
|
|
306
|
+
state.order = ACKNOWLEDGED_ALERT_STATE_ORDER;
|
|
307
|
+
return state;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let errorLog: jest.SpyInstance;
|
|
311
|
+
|
|
312
|
+
// The error log lines about acknowledging, whatever else the chain logs.
|
|
313
|
+
function acknowledgementErrorLogs(): Array<Array<unknown>> {
|
|
314
|
+
return errorLog.mock.calls.filter((call: Array<unknown>): boolean => {
|
|
315
|
+
return String(call[0]).toLowerCase().includes("acknowledg");
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
beforeEach(() => {
|
|
320
|
+
errorLog = jest.spyOn(logger, "error").mockImplementation((() => {
|
|
321
|
+
// quiet
|
|
322
|
+
}) as never);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
afterEach(async () => {
|
|
326
|
+
await settle();
|
|
327
|
+
jest.restoreAllMocks();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
/*
|
|
331
|
+
* The project's alerts, as the stubbed AlertService.findBy returns them.
|
|
332
|
+
* Every id asked for exists, in a Created state and without labels, unless a
|
|
333
|
+
* test stores it otherwise (a null state order: an alert without a current
|
|
334
|
+
* state). A label filter - a caller's label scope, as the real
|
|
335
|
+
* AlertStateChangeAuthorization adds it - is applied, so that check can run
|
|
336
|
+
* against these rows.
|
|
337
|
+
*/
|
|
338
|
+
type StoredAlert = {
|
|
339
|
+
stateOrder: number | null;
|
|
340
|
+
labels: Array<Label>;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
let storedAlerts: Map<string, StoredAlert> = new Map();
|
|
344
|
+
let findAlerts: jest.SpyInstance;
|
|
345
|
+
|
|
346
|
+
function storeAlert(
|
|
347
|
+
alertId: string,
|
|
348
|
+
alert: { stateOrder?: number | null; labels?: Array<Label> },
|
|
349
|
+
): void {
|
|
350
|
+
storedAlerts.set(alertId.toLowerCase(), {
|
|
351
|
+
stateOrder:
|
|
352
|
+
alert.stateOrder === undefined
|
|
353
|
+
? CREATED_ALERT_STATE_ORDER
|
|
354
|
+
: alert.stateOrder,
|
|
355
|
+
labels: alert.labels || [],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// The values bound into a Raw FindOperator such as QueryHelper.any.
|
|
360
|
+
function rawValues(operator: unknown): Array<string> {
|
|
361
|
+
expect(operator).toBeInstanceOf(FindOperator);
|
|
362
|
+
|
|
363
|
+
const values: Array<unknown> = Object.values(
|
|
364
|
+
(operator as FindOperator<unknown>).objectLiteralParameters || {},
|
|
365
|
+
)[0] as Array<unknown>;
|
|
366
|
+
|
|
367
|
+
return values.map((value: unknown): string => {
|
|
368
|
+
return String(value);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function findStoredAlerts(args: FindAlertsArgs): Array<Alert> {
|
|
373
|
+
const labelScope: { _id?: unknown } | undefined = args.query["labels"] as
|
|
374
|
+
| { _id?: unknown }
|
|
375
|
+
| undefined;
|
|
376
|
+
|
|
377
|
+
const allowedLabelIds: Set<string> | null = labelScope
|
|
378
|
+
? new Set(
|
|
379
|
+
rawValues(labelScope._id).map((id: string): string => {
|
|
380
|
+
return id.toLowerCase();
|
|
381
|
+
}),
|
|
382
|
+
)
|
|
383
|
+
: null;
|
|
384
|
+
|
|
385
|
+
const alerts: Array<Alert> = [];
|
|
386
|
+
|
|
387
|
+
for (const id of rawValues(args.query["_id"])) {
|
|
388
|
+
const stored: StoredAlert = storedAlerts.get(id.toLowerCase()) || {
|
|
389
|
+
stateOrder: CREATED_ALERT_STATE_ORDER,
|
|
390
|
+
labels: [],
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (
|
|
394
|
+
allowedLabelIds &&
|
|
395
|
+
!stored.labels.some((label: Label): boolean => {
|
|
396
|
+
return allowedLabelIds.has(label.id!.toString().toLowerCase());
|
|
397
|
+
})
|
|
398
|
+
) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const alert: Alert = new Alert();
|
|
403
|
+
alert._id = id;
|
|
404
|
+
alert.labels = stored.labels;
|
|
405
|
+
|
|
406
|
+
if (stored.stateOrder !== null) {
|
|
407
|
+
const state: AlertState = new AlertState();
|
|
408
|
+
state.order = stored.stateOrder;
|
|
409
|
+
alert.currentAlertState = state;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
alerts.push(alert);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return alerts;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// The acknowledgement validator's root read of the alerts' current states.
|
|
419
|
+
function alertStateReads(): Array<FindAlertsArgs> {
|
|
420
|
+
return findAlerts.mock.calls
|
|
421
|
+
.map((call: Array<unknown>): FindAlertsArgs => {
|
|
422
|
+
return call[0] as FindAlertsArgs;
|
|
423
|
+
})
|
|
424
|
+
.filter((args: FindAlertsArgs): boolean => {
|
|
425
|
+
return Boolean(args.select["currentAlertState"]);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// What onBeforeCreate reads besides the alerts, up to the incident number.
|
|
430
|
+
function stubBeforeCreate(): jest.SpyInstance {
|
|
431
|
+
const createdState: IncidentState = new IncidentState();
|
|
432
|
+
createdState._id = CREATED_STATE_ID;
|
|
433
|
+
|
|
434
|
+
jest
|
|
435
|
+
.spyOn(IncidentStateService, "findOneBy")
|
|
436
|
+
.mockResolvedValue(createdState as never);
|
|
437
|
+
jest
|
|
438
|
+
.spyOn(ProjectScopedReferenceValidator, "validateReferencesBelongToProject")
|
|
439
|
+
.mockResolvedValue(undefined as never);
|
|
440
|
+
jest
|
|
441
|
+
.spyOn(
|
|
442
|
+
SloRecordReferenceValidator,
|
|
443
|
+
"validateServiceLevelObjectivesBelongToProject",
|
|
444
|
+
)
|
|
445
|
+
.mockResolvedValue(undefined as never);
|
|
446
|
+
jest
|
|
447
|
+
.spyOn(CustomFieldMappingService, "applyMappingsToCreate")
|
|
448
|
+
.mockResolvedValue(undefined as never);
|
|
449
|
+
jest
|
|
450
|
+
.spyOn(UserService, "getUserMarkdownString")
|
|
451
|
+
.mockResolvedValue("**A responder**" as never);
|
|
452
|
+
|
|
453
|
+
// The project's alerts: every id asked for exists (see storeAlert).
|
|
454
|
+
storedAlerts = new Map();
|
|
455
|
+
findAlerts = jest.spyOn(AlertService, "findBy").mockImplementation((async (
|
|
456
|
+
args: FindAlertsArgs,
|
|
457
|
+
): Promise<Array<Alert>> => {
|
|
458
|
+
return findStoredAlerts(args);
|
|
459
|
+
}) as never);
|
|
460
|
+
|
|
461
|
+
return jest
|
|
462
|
+
.spyOn(ProjectService, "incrementAndGetIncidentCounter")
|
|
463
|
+
.mockResolvedValue({ counter: 17, prefix: "INC-" } as never);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// The rest of onCreateSuccess: the un-awaited chain and the rule engines.
|
|
467
|
+
function stubCreateSuccessChain(): void {
|
|
468
|
+
jest.spyOn(ProductAnalytics, "captureForUser").mockImplementation((() => {
|
|
469
|
+
// no analytics in tests
|
|
470
|
+
}) as never);
|
|
471
|
+
|
|
472
|
+
const reRead: Incident = new Incident();
|
|
473
|
+
reRead._id = INCIDENT_ID.toString();
|
|
474
|
+
reRead.projectId = PROJECT_ID;
|
|
475
|
+
jest.spyOn(IncidentService, "findOneById").mockResolvedValue(reRead as never);
|
|
476
|
+
|
|
477
|
+
const service: Record<string, unknown> = IncidentService as unknown as Record<
|
|
478
|
+
string,
|
|
479
|
+
unknown
|
|
480
|
+
>;
|
|
481
|
+
for (const method of [
|
|
482
|
+
"handleIncidentWorkspaceOperationsAsync",
|
|
483
|
+
"createIncidentFeedAsync",
|
|
484
|
+
"handleIncidentStateChangeAsync",
|
|
485
|
+
"disableActiveMonitoringIfManualIncident",
|
|
486
|
+
"refreshReminderSchedule",
|
|
487
|
+
]) {
|
|
488
|
+
jest
|
|
489
|
+
.spyOn(service as Record<string, () => Promise<void>>, method)
|
|
490
|
+
.mockResolvedValue(undefined as never);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
jest
|
|
494
|
+
.spyOn(IncidentPrivacyRuleEngineService, "applyRulesToIncident")
|
|
495
|
+
.mockResolvedValue(false as never);
|
|
496
|
+
jest
|
|
497
|
+
.spyOn(IncidentOwnerRuleEngineService, "applyRulesToIncident")
|
|
498
|
+
.mockResolvedValue(undefined as never);
|
|
499
|
+
jest
|
|
500
|
+
.spyOn(IncidentLabelRuleEngineService, "applyRulesToIncident")
|
|
501
|
+
.mockResolvedValue(undefined as never);
|
|
502
|
+
jest
|
|
503
|
+
.spyOn(IncidentOnCallRuleEngineService, "applyRulesToIncident")
|
|
504
|
+
.mockResolvedValue(undefined as never);
|
|
505
|
+
jest
|
|
506
|
+
.spyOn(RunbookRuleEngineService, "applyRulesToIncident")
|
|
507
|
+
.mockResolvedValue(undefined as never);
|
|
508
|
+
jest
|
|
509
|
+
.spyOn(IncidentGroupingEngineService, "processIncident")
|
|
510
|
+
.mockResolvedValue({} as never);
|
|
511
|
+
jest
|
|
512
|
+
.spyOn(IncidentSlaService, "createSlaForIncident")
|
|
513
|
+
.mockResolvedValue(undefined as never);
|
|
514
|
+
jest
|
|
515
|
+
.spyOn(AIIncidentInvestigationRunner, "investigateNewIncident")
|
|
516
|
+
.mockResolvedValue(false as never);
|
|
517
|
+
jest
|
|
518
|
+
.spyOn(AutoRemediationRuleEngineService, "applyRulesToIncident")
|
|
519
|
+
.mockResolvedValue(undefined as never);
|
|
520
|
+
|
|
521
|
+
jest
|
|
522
|
+
.spyOn(IncidentAlertService, "createDeclaredFromAlertsFeedItem")
|
|
523
|
+
.mockResolvedValue(undefined as never);
|
|
524
|
+
jest
|
|
525
|
+
.spyOn(IncidentAlertService, "copyAlertOwnersToIncident")
|
|
526
|
+
.mockResolvedValue({ userIds: [], teamIds: [] } as never);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// By default every alert asked for is linked.
|
|
530
|
+
function stubLinking(): jest.SpyInstance {
|
|
531
|
+
return jest
|
|
532
|
+
.spyOn(IncidentAlertService, "linkAlertsToIncident")
|
|
533
|
+
.mockImplementation((async (args: {
|
|
534
|
+
alertIds: Array<ObjectID>;
|
|
535
|
+
}): Promise<LinkAlertsToIncidentResult> => {
|
|
536
|
+
return {
|
|
537
|
+
linkedAlertIds: [...args.alertIds],
|
|
538
|
+
alreadyLinkedAlertIds: [],
|
|
539
|
+
failed: [],
|
|
540
|
+
};
|
|
541
|
+
}) as never);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function buildIncident(): Incident {
|
|
545
|
+
const incident: Incident = new Incident();
|
|
546
|
+
incident.projectId = PROJECT_ID;
|
|
547
|
+
incident.title = "Checkout is failing";
|
|
548
|
+
incident.incidentSeverityId = SEVERITY_ID;
|
|
549
|
+
return incident;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function onBeforeCreate(
|
|
553
|
+
miscDataProps: JSONObject | undefined,
|
|
554
|
+
props: DatabaseCommonInteractionProps = userProps(Permission.ProjectMember),
|
|
555
|
+
data: Incident = buildIncident(),
|
|
556
|
+
): Promise<OnCreate<Incident>> {
|
|
557
|
+
return callHook<OnCreate<Incident>>(IncidentService, "onBeforeCreate", {
|
|
558
|
+
data: data,
|
|
559
|
+
miscDataProps: miscDataProps,
|
|
560
|
+
props: props,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function createdIncident(createdByUserId?: ObjectID): Incident {
|
|
565
|
+
const incident: Incident = new Incident();
|
|
566
|
+
incident._id = INCIDENT_ID.toString();
|
|
567
|
+
incident.projectId = PROJECT_ID;
|
|
568
|
+
incident.title = "Checkout is failing";
|
|
569
|
+
incident.declaredAt = OneUptimeDate.getCurrentDate();
|
|
570
|
+
incident.isPrivate = false;
|
|
571
|
+
if (createdByUserId) {
|
|
572
|
+
incident.createdByUserId = createdByUserId;
|
|
573
|
+
}
|
|
574
|
+
return incident;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function onCreateSuccess(
|
|
578
|
+
carryForward: unknown,
|
|
579
|
+
createdItem: Incident,
|
|
580
|
+
props: DatabaseCommonInteractionProps = { isRoot: true },
|
|
581
|
+
): Promise<Incident> {
|
|
582
|
+
return callHook<Incident>(
|
|
583
|
+
IncidentService,
|
|
584
|
+
"onCreateSuccess",
|
|
585
|
+
{
|
|
586
|
+
createBy: { data: createdItem, props: props },
|
|
587
|
+
carryForward: carryForward,
|
|
588
|
+
},
|
|
589
|
+
createdItem,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/*
|
|
594
|
+
* A carry-forward as onBeforeCreate builds it. By default every alert to
|
|
595
|
+
* link was not acknowledged yet, so every one is to be acknowledged.
|
|
596
|
+
*/
|
|
597
|
+
function carried(
|
|
598
|
+
alertIds: Array<ObjectID>,
|
|
599
|
+
alertIdsToAcknowledge: Array<ObjectID> = alertIds,
|
|
600
|
+
acknowledgedAlertStateId: ObjectID | null = new ObjectID(
|
|
601
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
602
|
+
),
|
|
603
|
+
): CarriedForward {
|
|
604
|
+
return {
|
|
605
|
+
alertIdsToLink: alertIds,
|
|
606
|
+
acknowledgedAlertStateId: acknowledgedAlertStateId,
|
|
607
|
+
alertIdsToAcknowledge: alertIdsToAcknowledge,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
describe("IncidentService.onBeforeCreate with a request to acknowledge the alerts", () => {
|
|
612
|
+
let counter: jest.SpyInstance;
|
|
613
|
+
let validateAlertIds: jest.SpyInstance;
|
|
614
|
+
let validateAcknowledge: jest.SpyInstance;
|
|
615
|
+
let findAcknowledgedState: jest.SpyInstance;
|
|
616
|
+
let authorize: jest.SpyInstance;
|
|
617
|
+
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
counter = stubBeforeCreate();
|
|
620
|
+
|
|
621
|
+
// Both validators run for real unless a test replaces them.
|
|
622
|
+
validateAlertIds = jest.spyOn(
|
|
623
|
+
IncidentAlertService,
|
|
624
|
+
"validateAlertIdsForNewIncident",
|
|
625
|
+
);
|
|
626
|
+
validateAcknowledge = jest.spyOn(
|
|
627
|
+
IncidentAlertService,
|
|
628
|
+
"validateAcknowledgeAlertsForNewIncident",
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
findAcknowledgedState = jest
|
|
632
|
+
.spyOn(AlertStateService, "findOneBy")
|
|
633
|
+
.mockResolvedValue(acknowledgedAlertState() as never);
|
|
634
|
+
authorize = jest
|
|
635
|
+
.spyOn(AlertStateChangeAuthorization, "assertCanChangeStateOfAlerts")
|
|
636
|
+
.mockResolvedValue(undefined as never);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
function validateAcknowledgeArgs(): ValidateAcknowledgeArgs {
|
|
640
|
+
expect(validateAcknowledge).toHaveBeenCalledTimes(1);
|
|
641
|
+
return validateAcknowledge.mock.calls[0]![0] as ValidateAcknowledgeArgs;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
test("without miscDataProps the validator hears no request, and nothing is carried forward", async () => {
|
|
645
|
+
const props: DatabaseCommonInteractionProps = userProps(
|
|
646
|
+
Permission.ProjectMember,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const result: OnCreate<Incident> = await onBeforeCreate(undefined, props);
|
|
650
|
+
|
|
651
|
+
const args: ValidateAcknowledgeArgs = validateAcknowledgeArgs();
|
|
652
|
+
|
|
653
|
+
expect(args.acknowledgeAlerts).toBeUndefined();
|
|
654
|
+
expect(args.alertIds).toEqual([]);
|
|
655
|
+
expect(args.projectId!.toString()).toBe(PROJECT_ID.toString());
|
|
656
|
+
expect(args.props).toBe(props);
|
|
657
|
+
|
|
658
|
+
expect(result.carryForward).toBeNull();
|
|
659
|
+
expect(findAcknowledgedState).not.toHaveBeenCalled();
|
|
660
|
+
expect(alertStateReads()).toEqual([]);
|
|
661
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
662
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("alerts without the key are linked but not acknowledged: no state and no alerts to acknowledge", async () => {
|
|
666
|
+
const props: DatabaseCommonInteractionProps = userProps(
|
|
667
|
+
Permission.ProjectMember,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const result: OnCreate<Incident> = await onBeforeCreate(
|
|
671
|
+
{ [INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2] },
|
|
672
|
+
props,
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
const args: ValidateAcknowledgeArgs = validateAcknowledgeArgs();
|
|
676
|
+
|
|
677
|
+
expect(args.acknowledgeAlerts).toBeUndefined();
|
|
678
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
679
|
+
expect(args.props).toBe(props);
|
|
680
|
+
|
|
681
|
+
const carriedForward: CarriedForward =
|
|
682
|
+
result.carryForward as CarriedForward;
|
|
683
|
+
|
|
684
|
+
expect(Object.keys(carriedForward).sort()).toEqual([
|
|
685
|
+
"acknowledgedAlertStateId",
|
|
686
|
+
"alertIdsToAcknowledge",
|
|
687
|
+
"alertIdsToLink",
|
|
688
|
+
]);
|
|
689
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
690
|
+
expect(carriedForward.acknowledgedAlertStateId).toBeNull();
|
|
691
|
+
expect(carriedForward.alertIdsToAcknowledge).toEqual([]);
|
|
692
|
+
|
|
693
|
+
// Not asked to acknowledge: no state lookups and no per-alert check.
|
|
694
|
+
expect(findAcknowledgedState).not.toHaveBeenCalled();
|
|
695
|
+
expect(alertStateReads()).toEqual([]);
|
|
696
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
697
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
698
|
+
expect(result.createBy.data.incidentNumber).toBe(17);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test.each([
|
|
702
|
+
["false", false],
|
|
703
|
+
["null", null],
|
|
704
|
+
])(
|
|
705
|
+
"a %s acknowledgeAlertsToLink links the alerts without acknowledging them",
|
|
706
|
+
async (_label: string, value: boolean | null) => {
|
|
707
|
+
const result: OnCreate<Incident> = await onBeforeCreate({
|
|
708
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
709
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: value,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
expect(validateAcknowledgeArgs().acknowledgeAlerts).toBe(value);
|
|
713
|
+
|
|
714
|
+
const carriedForward: CarriedForward =
|
|
715
|
+
result.carryForward as CarriedForward;
|
|
716
|
+
|
|
717
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID]);
|
|
718
|
+
expect(carriedForward.acknowledgedAlertStateId).toBeNull();
|
|
719
|
+
expect(carriedForward.alertIdsToAcknowledge).toEqual([]);
|
|
720
|
+
expect(findAcknowledgedState).not.toHaveBeenCalled();
|
|
721
|
+
expect(alertStateReads()).toEqual([]);
|
|
722
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
723
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
724
|
+
},
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
test("a false acknowledgeAlertsToLink without alerts is no request at all", async () => {
|
|
728
|
+
const result: OnCreate<Incident> = await onBeforeCreate({
|
|
729
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: false,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
expect(result.carryForward).toBeNull();
|
|
733
|
+
expect(validateAlertIds).not.toHaveBeenCalled();
|
|
734
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("true with open alerts carries the validated alert ids, the project's Acknowledged alert state, and every alert to acknowledge", async () => {
|
|
738
|
+
const props: DatabaseCommonInteractionProps = userProps(
|
|
739
|
+
Permission.ProjectMember,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
const result: OnCreate<Incident> = await onBeforeCreate(
|
|
743
|
+
{
|
|
744
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [
|
|
745
|
+
ALERT_ID,
|
|
746
|
+
ALERT_ID_2,
|
|
747
|
+
ALERT_ID.toUpperCase(),
|
|
748
|
+
],
|
|
749
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
750
|
+
},
|
|
751
|
+
props,
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
const carriedForward: CarriedForward =
|
|
755
|
+
result.carryForward as CarriedForward;
|
|
756
|
+
|
|
757
|
+
expect(Object.keys(carriedForward).sort()).toEqual([
|
|
758
|
+
"acknowledgedAlertStateId",
|
|
759
|
+
"alertIdsToAcknowledge",
|
|
760
|
+
"alertIdsToLink",
|
|
761
|
+
]);
|
|
762
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
763
|
+
expect(carriedForward.acknowledgedAlertStateId).toBeInstanceOf(ObjectID);
|
|
764
|
+
expect(carriedForward.acknowledgedAlertStateId!.toString()).toBe(
|
|
765
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
766
|
+
);
|
|
767
|
+
// Both alerts are in the Created state: both are to be acknowledged.
|
|
768
|
+
expect(ids(carriedForward.alertIdsToAcknowledge)).toEqual([
|
|
769
|
+
ALERT_ID,
|
|
770
|
+
ALERT_ID_2,
|
|
771
|
+
]);
|
|
772
|
+
|
|
773
|
+
// The project's Acknowledged state and its order, read as root.
|
|
774
|
+
expect(findAcknowledgedState).toHaveBeenCalledTimes(1);
|
|
775
|
+
expect(findAcknowledgedState).toHaveBeenCalledWith({
|
|
776
|
+
query: {
|
|
777
|
+
projectId: PROJECT_ID,
|
|
778
|
+
isAcknowledgedState: true,
|
|
779
|
+
},
|
|
780
|
+
select: {
|
|
781
|
+
_id: true,
|
|
782
|
+
order: true,
|
|
783
|
+
},
|
|
784
|
+
props: {
|
|
785
|
+
isRoot: true,
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// The alerts' current state orders, read as root, for the validated ids.
|
|
790
|
+
const stateReads: Array<FindAlertsArgs> = alertStateReads();
|
|
791
|
+
|
|
792
|
+
expect(stateReads).toHaveLength(1);
|
|
793
|
+
expect(stateReads[0]).toEqual({
|
|
794
|
+
query: {
|
|
795
|
+
_id: expect.any(FindOperator),
|
|
796
|
+
projectId: PROJECT_ID,
|
|
797
|
+
},
|
|
798
|
+
select: {
|
|
799
|
+
_id: true,
|
|
800
|
+
currentAlertState: {
|
|
801
|
+
order: true,
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
limit: LIMIT_PER_PROJECT,
|
|
805
|
+
skip: 0,
|
|
806
|
+
props: {
|
|
807
|
+
isRoot: true,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
expect(rawValues(stateReads[0]!.query["_id"])).toEqual([
|
|
811
|
+
ALERT_ID,
|
|
812
|
+
ALERT_ID_2,
|
|
813
|
+
]);
|
|
814
|
+
|
|
815
|
+
// The caller may change the state of every alert that will be acknowledged.
|
|
816
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
817
|
+
|
|
818
|
+
const authorizeArgs: AuthorizeArgs = authorize.mock.calls[0]![0];
|
|
819
|
+
|
|
820
|
+
expect(authorizeArgs.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
821
|
+
expect(ids(authorizeArgs.alertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
822
|
+
expect(authorizeArgs.props).toBe(props);
|
|
823
|
+
|
|
824
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
825
|
+
expect(result.createBy.data.incidentNumber).toBe(17);
|
|
826
|
+
expect(result.createBy.data.incidentNumberWithPrefix).toBe("INC-17");
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("only alerts not acknowledged yet are checked and carried to acknowledge; acknowledged, later and resolved ones are only linked", async () => {
|
|
830
|
+
const props: DatabaseCommonInteractionProps = userProps(
|
|
831
|
+
Permission.ProjectMember,
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
storeAlert(ALERT_ID, { stateOrder: ACKNOWLEDGED_ALERT_STATE_ORDER });
|
|
835
|
+
storeAlert(ALERT_ID_2, { stateOrder: CREATED_ALERT_STATE_ORDER });
|
|
836
|
+
storeAlert(ALERT_ID_3, { stateOrder: RESOLVED_ALERT_STATE_ORDER });
|
|
837
|
+
// An alert without a current state is not acknowledged either.
|
|
838
|
+
storeAlert(ALERT_ID_4, { stateOrder: null });
|
|
839
|
+
|
|
840
|
+
const result: OnCreate<Incident> = await onBeforeCreate(
|
|
841
|
+
{
|
|
842
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [
|
|
843
|
+
ALERT_ID,
|
|
844
|
+
ALERT_ID_2,
|
|
845
|
+
ALERT_ID_3,
|
|
846
|
+
ALERT_ID_4,
|
|
847
|
+
],
|
|
848
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
849
|
+
},
|
|
850
|
+
props,
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const carriedForward: CarriedForward =
|
|
854
|
+
result.carryForward as CarriedForward;
|
|
855
|
+
|
|
856
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([
|
|
857
|
+
ALERT_ID,
|
|
858
|
+
ALERT_ID_2,
|
|
859
|
+
ALERT_ID_3,
|
|
860
|
+
ALERT_ID_4,
|
|
861
|
+
]);
|
|
862
|
+
expect(carriedForward.acknowledgedAlertStateId!.toString()).toBe(
|
|
863
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
864
|
+
);
|
|
865
|
+
expect(ids(carriedForward.alertIdsToAcknowledge)).toEqual([
|
|
866
|
+
ALERT_ID_2,
|
|
867
|
+
ALERT_ID_4,
|
|
868
|
+
]);
|
|
869
|
+
|
|
870
|
+
// The permission check is about the alerts that will be written, only.
|
|
871
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
872
|
+
expect(
|
|
873
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
874
|
+
).toEqual([ALERT_ID_2, ALERT_ID_4]);
|
|
875
|
+
|
|
876
|
+
// States first, then the check on what they leave, then the number.
|
|
877
|
+
expect(findAcknowledgedState.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
878
|
+
authorize.mock.invocationCallOrder[0]!,
|
|
879
|
+
);
|
|
880
|
+
expect(authorize.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
881
|
+
counter.mock.invocationCallOrder[0]!,
|
|
882
|
+
);
|
|
883
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("every alert already acknowledged or resolved: the key is accepted, nothing is left to acknowledge, and no permission check runs", async () => {
|
|
887
|
+
storeAlert(ALERT_ID, { stateOrder: ACKNOWLEDGED_ALERT_STATE_ORDER });
|
|
888
|
+
storeAlert(ALERT_ID_2, { stateOrder: RESOLVED_ALERT_STATE_ORDER });
|
|
889
|
+
|
|
890
|
+
// Would refuse anybody - it must not be asked.
|
|
891
|
+
authorize.mockRejectedValue(
|
|
892
|
+
new NotAuthorizedException(MAY_NOT_CHANGE_STATE_MESSAGE) as never,
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
const result: OnCreate<Incident> = await onBeforeCreate({
|
|
896
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
897
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const carriedForward: CarriedForward =
|
|
901
|
+
result.carryForward as CarriedForward;
|
|
902
|
+
|
|
903
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
904
|
+
expect(carriedForward.acknowledgedAlertStateId!.toString()).toBe(
|
|
905
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
906
|
+
);
|
|
907
|
+
expect(carriedForward.alertIdsToAcknowledge).toEqual([]);
|
|
908
|
+
|
|
909
|
+
expect(findAcknowledgedState).toHaveBeenCalledTimes(1);
|
|
910
|
+
expect(alertStateReads()).toHaveLength(1);
|
|
911
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
912
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
test("a caller who may not change an alert that is already resolved still declares with the key: only the open alert is checked and carried", async () => {
|
|
916
|
+
// A (open) may be changed by the caller; B (resolved) may not.
|
|
917
|
+
storeAlert(ALERT_ID, { stateOrder: CREATED_ALERT_STATE_ORDER });
|
|
918
|
+
storeAlert(ALERT_ID_2, { stateOrder: RESOLVED_ALERT_STATE_ORDER });
|
|
919
|
+
|
|
920
|
+
authorize.mockImplementation((async (args: AuthorizeArgs) => {
|
|
921
|
+
if (ids(args.alertIds).includes(ALERT_ID_2)) {
|
|
922
|
+
throw new NotAuthorizedException(MAY_NOT_CHANGE_STATE_MESSAGE);
|
|
923
|
+
}
|
|
924
|
+
}) as never);
|
|
925
|
+
|
|
926
|
+
const result: OnCreate<Incident> = await onBeforeCreate({
|
|
927
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
928
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const carriedForward: CarriedForward =
|
|
932
|
+
result.carryForward as CarriedForward;
|
|
933
|
+
|
|
934
|
+
// Both are linked; only the open one is acknowledged.
|
|
935
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
936
|
+
expect(ids(carriedForward.alertIdsToAcknowledge)).toEqual([ALERT_ID]);
|
|
937
|
+
expect(carriedForward.acknowledgedAlertStateId!.toString()).toBe(
|
|
938
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
942
|
+
expect(
|
|
943
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
944
|
+
).toEqual([ALERT_ID]);
|
|
945
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test("the same caller is refused while the alert they may not change is still open", async () => {
|
|
949
|
+
storeAlert(ALERT_ID, { stateOrder: CREATED_ALERT_STATE_ORDER });
|
|
950
|
+
storeAlert(ALERT_ID_2, { stateOrder: CREATED_ALERT_STATE_ORDER });
|
|
951
|
+
|
|
952
|
+
authorize.mockImplementation((async (args: AuthorizeArgs) => {
|
|
953
|
+
if (ids(args.alertIds).includes(ALERT_ID_2)) {
|
|
954
|
+
throw new NotAuthorizedException(MAY_NOT_CHANGE_STATE_MESSAGE);
|
|
955
|
+
}
|
|
956
|
+
}) as never);
|
|
957
|
+
|
|
958
|
+
const created: Promise<OnCreate<Incident>> = onBeforeCreate({
|
|
959
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
960
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
await expect(created).rejects.toBeInstanceOf(BadDataException);
|
|
964
|
+
await expect(created).rejects.toThrow(MAY_NOT_ACKNOWLEDGE_MESSAGE);
|
|
965
|
+
|
|
966
|
+
expect(
|
|
967
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
968
|
+
).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
969
|
+
expect(counter).not.toHaveBeenCalled();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test("the alert ids are validated first, then the acknowledgement with the validated ids, then the number is taken", async () => {
|
|
973
|
+
const props: DatabaseCommonInteractionProps = userProps(
|
|
974
|
+
Permission.ProjectMember,
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
await onBeforeCreate(
|
|
978
|
+
{
|
|
979
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [
|
|
980
|
+
` ${ALERT_ID_2} `,
|
|
981
|
+
ALERT_ID,
|
|
982
|
+
ALERT_ID_2.toUpperCase(),
|
|
983
|
+
],
|
|
984
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
985
|
+
},
|
|
986
|
+
props,
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
expect(validateAlertIds).toHaveBeenCalledTimes(1);
|
|
990
|
+
|
|
991
|
+
const validated: Array<ObjectID> = await (validateAlertIds.mock.results[0]!
|
|
992
|
+
.value as Promise<Array<ObjectID>>);
|
|
993
|
+
|
|
994
|
+
expect(ids(validated)).toEqual([ALERT_ID_2, ALERT_ID]);
|
|
995
|
+
|
|
996
|
+
const args: ValidateAcknowledgeArgs = validateAcknowledgeArgs();
|
|
997
|
+
|
|
998
|
+
// Not the raw request: the trimmed, deduplicated, checked ids.
|
|
999
|
+
expect(ids(args.alertIds)).toEqual(ids(validated));
|
|
1000
|
+
expect(args.acknowledgeAlerts).toBe(true);
|
|
1001
|
+
expect(args.projectId!.toString()).toBe(PROJECT_ID.toString());
|
|
1002
|
+
expect(args.props).toBe(props);
|
|
1003
|
+
|
|
1004
|
+
const alertIdsOrder: number = validateAlertIds.mock.invocationCallOrder[0]!;
|
|
1005
|
+
const acknowledgeOrder: number =
|
|
1006
|
+
validateAcknowledge.mock.invocationCallOrder[0]!;
|
|
1007
|
+
|
|
1008
|
+
expect(alertIdsOrder).toBeLessThan(acknowledgeOrder);
|
|
1009
|
+
expect(acknowledgeOrder).toBeLessThan(counter.mock.invocationCallOrder[0]!);
|
|
1010
|
+
expect(authorize.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
1011
|
+
counter.mock.invocationCallOrder[0]!,
|
|
1012
|
+
);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
test("the acknowledged state and the alerts the validator returns are what is carried forward", async () => {
|
|
1016
|
+
const stateId: ObjectID = ObjectID.generate();
|
|
1017
|
+
const alertIdsToAcknowledge: Array<ObjectID> = [new ObjectID(ALERT_ID_2)];
|
|
1018
|
+
|
|
1019
|
+
validateAcknowledge.mockResolvedValue({
|
|
1020
|
+
acknowledgedAlertStateId: stateId,
|
|
1021
|
+
alertIdsToAcknowledge: alertIdsToAcknowledge,
|
|
1022
|
+
} as never);
|
|
1023
|
+
|
|
1024
|
+
const result: OnCreate<Incident> = await onBeforeCreate({
|
|
1025
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
1026
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const carriedForward: CarriedForward =
|
|
1030
|
+
result.carryForward as CarriedForward;
|
|
1031
|
+
|
|
1032
|
+
expect(carriedForward.acknowledgedAlertStateId).toBe(stateId);
|
|
1033
|
+
// The validator's subset, not every linked alert.
|
|
1034
|
+
expect(carriedForward.alertIdsToAcknowledge).toBe(alertIdsToAcknowledge);
|
|
1035
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test("a validator that is not asked (null) carries no state and no alerts to acknowledge", async () => {
|
|
1039
|
+
validateAcknowledge.mockResolvedValue(null as never);
|
|
1040
|
+
|
|
1041
|
+
const result: OnCreate<Incident> = await onBeforeCreate({
|
|
1042
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1043
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
expect(result.carryForward).toEqual({
|
|
1047
|
+
alertIdsToLink: [new ObjectID(ALERT_ID)],
|
|
1048
|
+
acknowledgedAlertStateId: null,
|
|
1049
|
+
alertIdsToAcknowledge: [],
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test.each([
|
|
1054
|
+
["no alert ids at all", undefined],
|
|
1055
|
+
["a null list of alert ids", null],
|
|
1056
|
+
])(
|
|
1057
|
+
"true with %s is a 400, before an incident number is taken",
|
|
1058
|
+
async (_label: string, alertIds: null | undefined) => {
|
|
1059
|
+
const miscDataProps: JSONObject = {
|
|
1060
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
if (alertIds !== undefined) {
|
|
1064
|
+
miscDataProps[INCIDENT_ALERT_IDS_TO_LINK_KEY] = alertIds;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const created: Promise<OnCreate<Incident>> =
|
|
1068
|
+
onBeforeCreate(miscDataProps);
|
|
1069
|
+
|
|
1070
|
+
await expect(created).rejects.toBeInstanceOf(BadDataException);
|
|
1071
|
+
await expect(created).rejects.toThrow(NO_ALERTS_MESSAGE);
|
|
1072
|
+
|
|
1073
|
+
expect(validateAlertIds).not.toHaveBeenCalled();
|
|
1074
|
+
expect(validateAcknowledgeArgs().alertIds).toEqual([]);
|
|
1075
|
+
expect(findAcknowledgedState).not.toHaveBeenCalled();
|
|
1076
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
1077
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1078
|
+
},
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
test.each([
|
|
1082
|
+
["the string 'true'", "true"],
|
|
1083
|
+
["the number 1", 1],
|
|
1084
|
+
["an object", { acknowledge: true }],
|
|
1085
|
+
["a list", [true]],
|
|
1086
|
+
])(
|
|
1087
|
+
"%s is not a boolean: a 400, before an incident number is taken",
|
|
1088
|
+
async (_label: string, value: unknown) => {
|
|
1089
|
+
const created: Promise<OnCreate<Incident>> = onBeforeCreate({
|
|
1090
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1091
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: value as never,
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
await expect(created).rejects.toBeInstanceOf(BadDataException);
|
|
1095
|
+
await expect(created).rejects.toThrow(NOT_A_BOOLEAN_MESSAGE);
|
|
1096
|
+
|
|
1097
|
+
expect(findAcknowledgedState).not.toHaveBeenCalled();
|
|
1098
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
1099
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1100
|
+
},
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
function acknowledgedStateWithoutOrder(order: null | undefined): AlertState {
|
|
1104
|
+
const state: AlertState = acknowledgedAlertState();
|
|
1105
|
+
state.order = order as unknown as number;
|
|
1106
|
+
return state;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
test.each([
|
|
1110
|
+
[
|
|
1111
|
+
"no Acknowledged alert state",
|
|
1112
|
+
(): AlertState | null => {
|
|
1113
|
+
return null;
|
|
1114
|
+
},
|
|
1115
|
+
],
|
|
1116
|
+
[
|
|
1117
|
+
"an Acknowledged alert state without an order",
|
|
1118
|
+
(): AlertState | null => {
|
|
1119
|
+
return acknowledgedStateWithoutOrder(undefined);
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
[
|
|
1123
|
+
"an Acknowledged alert state with a null order",
|
|
1124
|
+
(): AlertState | null => {
|
|
1125
|
+
return acknowledgedStateWithoutOrder(null);
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
])(
|
|
1129
|
+
"a project with %s is a 400, before the alerts are read or a number is taken",
|
|
1130
|
+
async (_label: string, state: () => AlertState | null) => {
|
|
1131
|
+
findAcknowledgedState.mockResolvedValue(state() as never);
|
|
1132
|
+
|
|
1133
|
+
const created: Promise<OnCreate<Incident>> = onBeforeCreate({
|
|
1134
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1135
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
await expect(created).rejects.toBeInstanceOf(BadDataException);
|
|
1139
|
+
await expect(created).rejects.toThrow(NO_ACKNOWLEDGED_STATE_MESSAGE);
|
|
1140
|
+
|
|
1141
|
+
expect(findAcknowledgedState).toHaveBeenCalledTimes(1);
|
|
1142
|
+
expect(alertStateReads()).toEqual([]);
|
|
1143
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
1144
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1145
|
+
},
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
test("a caller who may not change the alerts' states is a 400, before an incident number is taken", async () => {
|
|
1149
|
+
authorize.mockRejectedValue(
|
|
1150
|
+
new NotAuthorizedException(MAY_NOT_CHANGE_STATE_MESSAGE) as never,
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
const created: Promise<OnCreate<Incident>> = onBeforeCreate({
|
|
1154
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
1155
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
await expect(created).rejects.toBeInstanceOf(BadDataException);
|
|
1159
|
+
await expect(created).rejects.toThrow(MAY_NOT_ACKNOWLEDGE_MESSAGE);
|
|
1160
|
+
|
|
1161
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
1162
|
+
expect(
|
|
1163
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
1164
|
+
).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1165
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test("an unexpected failure while checking keeps its own error, and no incident number is taken", async () => {
|
|
1169
|
+
const failure: Error = new Error("database unavailable");
|
|
1170
|
+
authorize.mockRejectedValue(failure as never);
|
|
1171
|
+
|
|
1172
|
+
await expect(
|
|
1173
|
+
onBeforeCreate({
|
|
1174
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1175
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1176
|
+
}),
|
|
1177
|
+
).rejects.toBe(failure);
|
|
1178
|
+
|
|
1179
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
test("any refusal from the acknowledgement validator rejects the create before the number is taken", async () => {
|
|
1183
|
+
const refusal: BadDataException = new BadDataException(
|
|
1184
|
+
"Acknowledging is refused.",
|
|
1185
|
+
);
|
|
1186
|
+
validateAcknowledge.mockRejectedValue(refusal as never);
|
|
1187
|
+
|
|
1188
|
+
await expect(
|
|
1189
|
+
onBeforeCreate({
|
|
1190
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1191
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1192
|
+
}),
|
|
1193
|
+
).rejects.toBe(refusal);
|
|
1194
|
+
|
|
1195
|
+
expect(validateAlertIds).toHaveBeenCalledTimes(1);
|
|
1196
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test("a bad alert id is refused first: the acknowledgement is never considered", async () => {
|
|
1200
|
+
await expect(
|
|
1201
|
+
onBeforeCreate({
|
|
1202
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: ["not-a-uuid"],
|
|
1203
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1204
|
+
}),
|
|
1205
|
+
).rejects.toThrow("alertIdsToLink must only contain alert ids.");
|
|
1206
|
+
|
|
1207
|
+
expect(validateAcknowledge).not.toHaveBeenCalled();
|
|
1208
|
+
expect(findAcknowledgedState).not.toHaveBeenCalled();
|
|
1209
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
1210
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
test("a caller who may not link the alerts is refused for that, before acknowledging is considered", async () => {
|
|
1214
|
+
await expect(
|
|
1215
|
+
onBeforeCreate(
|
|
1216
|
+
{
|
|
1217
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1218
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1219
|
+
},
|
|
1220
|
+
userProps(Permission.IncidentViewer),
|
|
1221
|
+
),
|
|
1222
|
+
).rejects.toThrow(
|
|
1223
|
+
"You do not have permission to link alerts to incidents in this project.",
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
expect(validateAcknowledge).not.toHaveBeenCalled();
|
|
1227
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test("with the real permission check, an incident member may link alerts but not acknowledge one that is not acknowledged yet", async () => {
|
|
1231
|
+
// IncidentMember may create an IncidentAlert but not an AlertStateTimeline.
|
|
1232
|
+
authorize.mockRestore();
|
|
1233
|
+
// The real check, watched.
|
|
1234
|
+
authorize = jest.spyOn(
|
|
1235
|
+
AlertStateChangeAuthorization,
|
|
1236
|
+
"assertCanChangeStateOfAlerts",
|
|
1237
|
+
);
|
|
1238
|
+
|
|
1239
|
+
const created: Promise<OnCreate<Incident>> = onBeforeCreate(
|
|
1240
|
+
{
|
|
1241
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1242
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1243
|
+
},
|
|
1244
|
+
userProps(Permission.IncidentMember),
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
await expect(created).rejects.toBeInstanceOf(BadDataException);
|
|
1248
|
+
await expect(created).rejects.toThrow(MAY_NOT_ACKNOWLEDGE_MESSAGE);
|
|
1249
|
+
|
|
1250
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
1251
|
+
expect(
|
|
1252
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
1253
|
+
).toEqual([ALERT_ID]);
|
|
1254
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1255
|
+
|
|
1256
|
+
// The same member declaring without acknowledging is let through.
|
|
1257
|
+
const withoutKey: OnCreate<Incident> = await onBeforeCreate(
|
|
1258
|
+
{ [INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID] },
|
|
1259
|
+
userProps(Permission.IncidentMember),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
expect(withoutKey.carryForward).toEqual({
|
|
1263
|
+
alertIdsToLink: [new ObjectID(ALERT_ID)],
|
|
1264
|
+
acknowledgedAlertStateId: null,
|
|
1265
|
+
alertIdsToAcknowledge: [],
|
|
1266
|
+
});
|
|
1267
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
1268
|
+
|
|
1269
|
+
/*
|
|
1270
|
+
* ...and so is the same member WITH the key once the alert is already
|
|
1271
|
+
* acknowledged: nothing would be written, so nothing is checked.
|
|
1272
|
+
*/
|
|
1273
|
+
storeAlert(ALERT_ID, { stateOrder: ACKNOWLEDGED_ALERT_STATE_ORDER });
|
|
1274
|
+
|
|
1275
|
+
const alreadyAcknowledged: OnCreate<Incident> = await onBeforeCreate(
|
|
1276
|
+
{
|
|
1277
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1278
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1279
|
+
},
|
|
1280
|
+
userProps(Permission.IncidentMember),
|
|
1281
|
+
);
|
|
1282
|
+
|
|
1283
|
+
const carriedForward: CarriedForward =
|
|
1284
|
+
alreadyAcknowledged.carryForward as CarriedForward;
|
|
1285
|
+
|
|
1286
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID]);
|
|
1287
|
+
expect(carriedForward.acknowledgedAlertStateId!.toString()).toBe(
|
|
1288
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
1289
|
+
);
|
|
1290
|
+
expect(carriedForward.alertIdsToAcknowledge).toEqual([]);
|
|
1291
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
1292
|
+
expect(counter).toHaveBeenCalledTimes(2);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
test("a root caller gets the Acknowledged state without the per-alert permission check", async () => {
|
|
1296
|
+
const result: OnCreate<Incident> = await onBeforeCreate(
|
|
1297
|
+
{
|
|
1298
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1299
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1300
|
+
},
|
|
1301
|
+
{ isRoot: true },
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
// The project comes from the incident itself for a root caller.
|
|
1305
|
+
expect(validateAcknowledgeArgs().projectId!.toString()).toBe(
|
|
1306
|
+
PROJECT_ID.toString(),
|
|
1307
|
+
);
|
|
1308
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
1309
|
+
|
|
1310
|
+
const carriedForward: CarriedForward =
|
|
1311
|
+
result.carryForward as CarriedForward;
|
|
1312
|
+
|
|
1313
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID]);
|
|
1314
|
+
expect(carriedForward.acknowledgedAlertStateId!.toString()).toBe(
|
|
1315
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
1316
|
+
);
|
|
1317
|
+
// The same "not acknowledged yet" rule, just without the check.
|
|
1318
|
+
expect(ids(carriedForward.alertIdsToAcknowledge)).toEqual([ALERT_ID]);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
test("a root caller's alerts that are already acknowledged are not carried to acknowledge either", async () => {
|
|
1322
|
+
storeAlert(ALERT_ID, { stateOrder: RESOLVED_ALERT_STATE_ORDER });
|
|
1323
|
+
|
|
1324
|
+
const result: OnCreate<Incident> = await onBeforeCreate(
|
|
1325
|
+
{
|
|
1326
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
1327
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1328
|
+
},
|
|
1329
|
+
{ isRoot: true },
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
const carriedForward: CarriedForward =
|
|
1333
|
+
result.carryForward as CarriedForward;
|
|
1334
|
+
|
|
1335
|
+
expect(ids(carriedForward.alertIdsToLink)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1336
|
+
expect(ids(carriedForward.alertIdsToAcknowledge)).toEqual([ALERT_ID_2]);
|
|
1337
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test("a root caller without an Acknowledged alert state is still refused before the number is taken", async () => {
|
|
1341
|
+
findAcknowledgedState.mockResolvedValue(null as never);
|
|
1342
|
+
|
|
1343
|
+
await expect(
|
|
1344
|
+
onBeforeCreate(
|
|
1345
|
+
{
|
|
1346
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
1347
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1348
|
+
},
|
|
1349
|
+
{ isRoot: true },
|
|
1350
|
+
),
|
|
1351
|
+
).rejects.toThrow(NO_ACKNOWLEDGED_STATE_MESSAGE);
|
|
1352
|
+
|
|
1353
|
+
expect(counter).not.toHaveBeenCalled();
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
describe("IncidentService.onCreateSuccess acknowledges the alerts it was declared with", () => {
|
|
1358
|
+
let link: jest.SpyInstance;
|
|
1359
|
+
let acknowledge: jest.SpyInstance;
|
|
1360
|
+
|
|
1361
|
+
const alertIds: Array<ObjectID> = [
|
|
1362
|
+
new ObjectID(ALERT_ID),
|
|
1363
|
+
new ObjectID(ALERT_ID_2),
|
|
1364
|
+
];
|
|
1365
|
+
|
|
1366
|
+
beforeEach(() => {
|
|
1367
|
+
stubCreateSuccessChain();
|
|
1368
|
+
link = stubLinking();
|
|
1369
|
+
acknowledge = jest
|
|
1370
|
+
.spyOn(IncidentAlertService, "acknowledgeAlertsDeclaredWithIncident")
|
|
1371
|
+
.mockResolvedValue(emptyAcknowledgeResult() as never);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
function acknowledgeArgs(): AcknowledgeArgs {
|
|
1375
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
1376
|
+
return acknowledge.mock.calls[0]![0] as AcknowledgeArgs;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
test("acknowledges the alerts carried forward to acknowledge once they are linked, credited to the incident's creator for a root caller", async () => {
|
|
1380
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1381
|
+
const carryForward: CarriedForward = carried(alertIds);
|
|
1382
|
+
|
|
1383
|
+
await expect(onCreateSuccess(carryForward, incident)).resolves.toBe(
|
|
1384
|
+
incident,
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
expect(link).toHaveBeenCalledTimes(1);
|
|
1388
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
1389
|
+
expect(acknowledge).toHaveBeenCalledWith({
|
|
1390
|
+
projectId: PROJECT_ID,
|
|
1391
|
+
incidentId: INCIDENT_ID,
|
|
1392
|
+
alertIds: alertIds,
|
|
1393
|
+
linkedAlertIds: alertIds,
|
|
1394
|
+
acknowledgedByUserId: USER_ID,
|
|
1395
|
+
});
|
|
1396
|
+
// The carried list itself is what is handed over.
|
|
1397
|
+
expect(acknowledgeArgs().alertIds).toBe(carryForward.alertIdsToAcknowledge);
|
|
1398
|
+
|
|
1399
|
+
// Acknowledged after linking, never before.
|
|
1400
|
+
expect(link.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
1401
|
+
acknowledge.mock.invocationCallOrder[0]!,
|
|
1402
|
+
);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
test("the alert ids acknowledged are the ones carried forward to acknowledge, not every linked alert", async () => {
|
|
1406
|
+
const threeAlerts: Array<ObjectID> = [
|
|
1407
|
+
new ObjectID(ALERT_ID),
|
|
1408
|
+
new ObjectID(ALERT_ID_2),
|
|
1409
|
+
new ObjectID(ALERT_ID_3),
|
|
1410
|
+
];
|
|
1411
|
+
|
|
1412
|
+
// Only ALERT_ID_2 was not acknowledged yet when it was checked.
|
|
1413
|
+
await onCreateSuccess(
|
|
1414
|
+
carried(threeAlerts, [new ObjectID(ALERT_ID_2)]),
|
|
1415
|
+
createdIncident(USER_ID),
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
// Every alert is linked...
|
|
1419
|
+
expect(ids((link.mock.calls[0]![0] as LinkArgs).alertIds)).toEqual([
|
|
1420
|
+
ALERT_ID,
|
|
1421
|
+
ALERT_ID_2,
|
|
1422
|
+
ALERT_ID_3,
|
|
1423
|
+
]);
|
|
1424
|
+
|
|
1425
|
+
const args: AcknowledgeArgs = acknowledgeArgs();
|
|
1426
|
+
|
|
1427
|
+
// ...only the one checked for is acknowledged...
|
|
1428
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID_2]);
|
|
1429
|
+
// ...and the linked ones are still handed over, for the sync check.
|
|
1430
|
+
expect(ids(args.linkedAlertIds)).toEqual([
|
|
1431
|
+
ALERT_ID,
|
|
1432
|
+
ALERT_ID_2,
|
|
1433
|
+
ALERT_ID_3,
|
|
1434
|
+
]);
|
|
1435
|
+
expect(args.acknowledgedByUserId).toBe(USER_ID);
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
test("acknowledging waits for the links to be written", async () => {
|
|
1439
|
+
const linking: Deferred<LinkAlertsToIncidentResult> =
|
|
1440
|
+
deferred<LinkAlertsToIncidentResult>();
|
|
1441
|
+
|
|
1442
|
+
link.mockImplementation((() => {
|
|
1443
|
+
return linking.promise;
|
|
1444
|
+
}) as never);
|
|
1445
|
+
|
|
1446
|
+
const created: Promise<Incident> = onCreateSuccess(
|
|
1447
|
+
carried(alertIds),
|
|
1448
|
+
createdIncident(USER_ID),
|
|
1449
|
+
);
|
|
1450
|
+
await settle();
|
|
1451
|
+
|
|
1452
|
+
// Still linking: nothing acknowledged yet.
|
|
1453
|
+
expect(link).toHaveBeenCalledTimes(1);
|
|
1454
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1455
|
+
|
|
1456
|
+
linking.resolve({
|
|
1457
|
+
linkedAlertIds: [...alertIds],
|
|
1458
|
+
alreadyLinkedAlertIds: [],
|
|
1459
|
+
failed: [],
|
|
1460
|
+
});
|
|
1461
|
+
await created;
|
|
1462
|
+
|
|
1463
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
1464
|
+
expect(ids(acknowledgeArgs().linkedAlertIds)).toEqual([
|
|
1465
|
+
ALERT_ID,
|
|
1466
|
+
ALERT_ID_2,
|
|
1467
|
+
]);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
test("the linked alert ids are the newly linked and the already linked ones, never the failed ones", async () => {
|
|
1471
|
+
const threeAlerts: Array<ObjectID> = [
|
|
1472
|
+
new ObjectID(ALERT_ID),
|
|
1473
|
+
new ObjectID(ALERT_ID_2),
|
|
1474
|
+
new ObjectID(ALERT_ID_3),
|
|
1475
|
+
];
|
|
1476
|
+
|
|
1477
|
+
link.mockResolvedValue({
|
|
1478
|
+
linkedAlertIds: [new ObjectID(ALERT_ID)],
|
|
1479
|
+
alreadyLinkedAlertIds: [new ObjectID(ALERT_ID_3)],
|
|
1480
|
+
failed: [{ alertId: new ObjectID(ALERT_ID_2), message: "gone" }],
|
|
1481
|
+
} as never);
|
|
1482
|
+
|
|
1483
|
+
await onCreateSuccess(carried(threeAlerts), createdIncident(USER_ID));
|
|
1484
|
+
|
|
1485
|
+
const args: AcknowledgeArgs = acknowledgeArgs();
|
|
1486
|
+
|
|
1487
|
+
// Every alert to acknowledge is still asked about, linked or not...
|
|
1488
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID, ALERT_ID_2, ALERT_ID_3]);
|
|
1489
|
+
// ...but only those with a link are handed over as linked.
|
|
1490
|
+
expect(ids(args.linkedAlertIds)).toEqual([ALERT_ID, ALERT_ID_3]);
|
|
1491
|
+
|
|
1492
|
+
// The link failure is still reported on its own.
|
|
1493
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1494
|
+
"1 of 3 alerts could not be linked to the incident they were declared with.",
|
|
1495
|
+
{
|
|
1496
|
+
projectId: PROJECT_ID.toString(),
|
|
1497
|
+
incidentId: INCIDENT_ID.toString(),
|
|
1498
|
+
},
|
|
1499
|
+
);
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
test("when linking throws, the alerts are still acknowledged, with no linked alerts", async () => {
|
|
1503
|
+
link.mockRejectedValue(new Error("database unavailable") as never);
|
|
1504
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1505
|
+
|
|
1506
|
+
await expect(onCreateSuccess(carried(alertIds), incident)).resolves.toBe(
|
|
1507
|
+
incident,
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
const args: AcknowledgeArgs = acknowledgeArgs();
|
|
1511
|
+
|
|
1512
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1513
|
+
expect(args.linkedAlertIds).toEqual([]);
|
|
1514
|
+
expect(args.acknowledgedByUserId).toBe(USER_ID);
|
|
1515
|
+
|
|
1516
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1517
|
+
expect.stringContaining(
|
|
1518
|
+
"Linking the alerts an incident was declared from failed in IncidentService.onCreateSuccess: Error: database unavailable",
|
|
1519
|
+
),
|
|
1520
|
+
expect.anything(),
|
|
1521
|
+
);
|
|
1522
|
+
expect(link.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
1523
|
+
acknowledge.mock.invocationCallOrder[0]!,
|
|
1524
|
+
);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
describe("who the acknowledgement is credited to", () => {
|
|
1528
|
+
test("a user is credited as themselves, whatever the incident's createdByUserId says", async () => {
|
|
1529
|
+
await onCreateSuccess(carried(alertIds), createdIncident(USER_ID), {
|
|
1530
|
+
userId: OTHER_USER_ID,
|
|
1531
|
+
tenantId: PROJECT_ID,
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
expect(acknowledgeArgs().acknowledgedByUserId).toBe(OTHER_USER_ID);
|
|
1535
|
+
// The same person who is recorded as linking the alerts.
|
|
1536
|
+
expect((link.mock.calls[0]![0] as LinkArgs).createdByUserId).toBe(
|
|
1537
|
+
OTHER_USER_ID,
|
|
1538
|
+
);
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
test("a user declaring from the dashboard is credited with props.userId", async () => {
|
|
1542
|
+
await onCreateSuccess(
|
|
1543
|
+
carried(alertIds),
|
|
1544
|
+
createdIncident(),
|
|
1545
|
+
userProps(Permission.ProjectMember),
|
|
1546
|
+
);
|
|
1547
|
+
|
|
1548
|
+
expect(acknowledgeArgs().acknowledgedByUserId).toBe(USER_ID);
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
test("an API key credits nobody, even when the payload names a creator", async () => {
|
|
1552
|
+
await onCreateSuccess(
|
|
1553
|
+
carried(alertIds),
|
|
1554
|
+
createdIncident(OTHER_USER_ID),
|
|
1555
|
+
apiKeyProps(),
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
const args: AcknowledgeArgs = acknowledgeArgs();
|
|
1559
|
+
|
|
1560
|
+
expect(args.acknowledgedByUserId).toBeUndefined();
|
|
1561
|
+
// Still acknowledged, just not credited to anyone.
|
|
1562
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test("a root caller credits the incident's createdByUserId", async () => {
|
|
1566
|
+
await onCreateSuccess(carried(alertIds), createdIncident(OTHER_USER_ID), {
|
|
1567
|
+
isRoot: true,
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
expect(acknowledgeArgs().acknowledgedByUserId).toBe(OTHER_USER_ID);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
test("a root caller with no creator on the incident credits nobody", async () => {
|
|
1574
|
+
await onCreateSuccess(carried(alertIds), createdIncident(), {
|
|
1575
|
+
isRoot: true,
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
expect(acknowledgeArgs().acknowledgedByUserId).toBeUndefined();
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
describe("the incident never waits for, or fails with, the acknowledgement", () => {
|
|
1583
|
+
test("the incident is returned while the acknowledgement is still running", async () => {
|
|
1584
|
+
acknowledge.mockImplementation((() => {
|
|
1585
|
+
return new Promise<AcknowledgeDeclaredAlertsResult>(() => {
|
|
1586
|
+
// never settles
|
|
1587
|
+
});
|
|
1588
|
+
}) as never);
|
|
1589
|
+
|
|
1590
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1591
|
+
|
|
1592
|
+
await expect(onCreateSuccess(carried(alertIds), incident)).resolves.toBe(
|
|
1593
|
+
incident,
|
|
1594
|
+
);
|
|
1595
|
+
|
|
1596
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
test("an acknowledgement that finishes after the incident is returned is fine", async () => {
|
|
1600
|
+
const acknowledging: Deferred<AcknowledgeDeclaredAlertsResult> =
|
|
1601
|
+
deferred<AcknowledgeDeclaredAlertsResult>();
|
|
1602
|
+
let finished: boolean = false;
|
|
1603
|
+
|
|
1604
|
+
acknowledge.mockImplementation(
|
|
1605
|
+
(async (): Promise<AcknowledgeDeclaredAlertsResult> => {
|
|
1606
|
+
const result: AcknowledgeDeclaredAlertsResult =
|
|
1607
|
+
await acknowledging.promise;
|
|
1608
|
+
finished = true;
|
|
1609
|
+
return result;
|
|
1610
|
+
}) as never,
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1614
|
+
|
|
1615
|
+
await expect(onCreateSuccess(carried(alertIds), incident)).resolves.toBe(
|
|
1616
|
+
incident,
|
|
1617
|
+
);
|
|
1618
|
+
expect(finished).toBe(false);
|
|
1619
|
+
|
|
1620
|
+
acknowledging.resolve({
|
|
1621
|
+
...emptyAcknowledgeResult(),
|
|
1622
|
+
acknowledgedAlertIds: [...alertIds],
|
|
1623
|
+
});
|
|
1624
|
+
await settle();
|
|
1625
|
+
|
|
1626
|
+
expect(finished).toBe(true);
|
|
1627
|
+
expect(acknowledgementErrorLogs()).toEqual([]);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
test("a rejected acknowledgement is caught and logged, and the incident is still returned", async () => {
|
|
1631
|
+
acknowledge.mockRejectedValue(
|
|
1632
|
+
new Error("alert states unavailable") as never,
|
|
1633
|
+
);
|
|
1634
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1635
|
+
|
|
1636
|
+
await expect(onCreateSuccess(carried(alertIds), incident)).resolves.toBe(
|
|
1637
|
+
incident,
|
|
1638
|
+
);
|
|
1639
|
+
await settle();
|
|
1640
|
+
|
|
1641
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1642
|
+
`${ACKNOWLEDGE_FAILED_LOG_PREFIX}Error: alert states unavailable`,
|
|
1643
|
+
{
|
|
1644
|
+
projectId: PROJECT_ID.toString(),
|
|
1645
|
+
incidentId: INCIDENT_ID.toString(),
|
|
1646
|
+
},
|
|
1647
|
+
);
|
|
1648
|
+
expect(acknowledgementErrorLogs()).toHaveLength(1);
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
test("an acknowledgement that rejects after the incident was returned is caught too", async () => {
|
|
1652
|
+
const acknowledging: Deferred<AcknowledgeDeclaredAlertsResult> =
|
|
1653
|
+
deferred<AcknowledgeDeclaredAlertsResult>();
|
|
1654
|
+
|
|
1655
|
+
acknowledge.mockImplementation((() => {
|
|
1656
|
+
return acknowledging.promise;
|
|
1657
|
+
}) as never);
|
|
1658
|
+
|
|
1659
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1660
|
+
|
|
1661
|
+
await expect(onCreateSuccess(carried(alertIds), incident)).resolves.toBe(
|
|
1662
|
+
incident,
|
|
1663
|
+
);
|
|
1664
|
+
expect(acknowledgementErrorLogs()).toEqual([]);
|
|
1665
|
+
|
|
1666
|
+
acknowledging.reject(new Error("redis lock timed out"));
|
|
1667
|
+
await settle();
|
|
1668
|
+
|
|
1669
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1670
|
+
`${ACKNOWLEDGE_FAILED_LOG_PREFIX}Error: redis lock timed out`,
|
|
1671
|
+
{
|
|
1672
|
+
projectId: PROJECT_ID.toString(),
|
|
1673
|
+
incidentId: INCIDENT_ID.toString(),
|
|
1674
|
+
},
|
|
1675
|
+
);
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
test("alerts that could not be acknowledged are logged with a count", async () => {
|
|
1679
|
+
acknowledge.mockResolvedValue({
|
|
1680
|
+
acknowledgedAlertIds: [new ObjectID(ALERT_ID)],
|
|
1681
|
+
alreadyAcknowledgedAlertIds: [],
|
|
1682
|
+
leftToLinkedAlertSyncAlertIds: [],
|
|
1683
|
+
failed: [
|
|
1684
|
+
{
|
|
1685
|
+
alertId: new ObjectID(ALERT_ID_2),
|
|
1686
|
+
message: "The alert's state did not change to Acknowledged.",
|
|
1687
|
+
},
|
|
1688
|
+
],
|
|
1689
|
+
} as never);
|
|
1690
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1691
|
+
|
|
1692
|
+
await expect(onCreateSuccess(carried(alertIds), incident)).resolves.toBe(
|
|
1693
|
+
incident,
|
|
1694
|
+
);
|
|
1695
|
+
await settle();
|
|
1696
|
+
|
|
1697
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1698
|
+
"1 of 2 alerts could not be acknowledged when the incident was declared from them.",
|
|
1699
|
+
{
|
|
1700
|
+
projectId: PROJECT_ID.toString(),
|
|
1701
|
+
incidentId: INCIDENT_ID.toString(),
|
|
1702
|
+
},
|
|
1703
|
+
);
|
|
1704
|
+
expect(acknowledgementErrorLogs()).toHaveLength(1);
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
test("every alert failing is counted against every alert to acknowledge", async () => {
|
|
1708
|
+
const threeAlerts: Array<ObjectID> = [
|
|
1709
|
+
new ObjectID(ALERT_ID),
|
|
1710
|
+
new ObjectID(ALERT_ID_2),
|
|
1711
|
+
new ObjectID(ALERT_ID_3),
|
|
1712
|
+
];
|
|
1713
|
+
|
|
1714
|
+
acknowledge.mockResolvedValue({
|
|
1715
|
+
...emptyAcknowledgeResult(),
|
|
1716
|
+
failed: threeAlerts.map((alertId: ObjectID) => {
|
|
1717
|
+
return {
|
|
1718
|
+
alertId: alertId,
|
|
1719
|
+
message:
|
|
1720
|
+
"This project has no Acknowledged alert state, so the alerts could not be acknowledged.",
|
|
1721
|
+
};
|
|
1722
|
+
}),
|
|
1723
|
+
} as never);
|
|
1724
|
+
|
|
1725
|
+
await onCreateSuccess(carried(threeAlerts), createdIncident(USER_ID));
|
|
1726
|
+
await settle();
|
|
1727
|
+
|
|
1728
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1729
|
+
"3 of 3 alerts could not be acknowledged when the incident was declared from them.",
|
|
1730
|
+
expect.anything(),
|
|
1731
|
+
);
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
test("failures are counted against the alerts to acknowledge, not every linked alert", async () => {
|
|
1735
|
+
const fourAlerts: Array<ObjectID> = [
|
|
1736
|
+
new ObjectID(ALERT_ID),
|
|
1737
|
+
new ObjectID(ALERT_ID_2),
|
|
1738
|
+
new ObjectID(ALERT_ID_3),
|
|
1739
|
+
new ObjectID(ALERT_ID_4),
|
|
1740
|
+
];
|
|
1741
|
+
|
|
1742
|
+
// Four linked; two were not acknowledged yet, and one of those failed.
|
|
1743
|
+
acknowledge.mockResolvedValue({
|
|
1744
|
+
...emptyAcknowledgeResult(),
|
|
1745
|
+
acknowledgedAlertIds: [new ObjectID(ALERT_ID_2)],
|
|
1746
|
+
failed: [
|
|
1747
|
+
{
|
|
1748
|
+
alertId: new ObjectID(ALERT_ID_4),
|
|
1749
|
+
message: "The alert's state did not change to Acknowledged.",
|
|
1750
|
+
},
|
|
1751
|
+
],
|
|
1752
|
+
} as never);
|
|
1753
|
+
|
|
1754
|
+
await onCreateSuccess(
|
|
1755
|
+
carried(fourAlerts, [
|
|
1756
|
+
new ObjectID(ALERT_ID_2),
|
|
1757
|
+
new ObjectID(ALERT_ID_4),
|
|
1758
|
+
]),
|
|
1759
|
+
createdIncident(USER_ID),
|
|
1760
|
+
);
|
|
1761
|
+
await settle();
|
|
1762
|
+
|
|
1763
|
+
expect(errorLog).toHaveBeenCalledWith(
|
|
1764
|
+
"1 of 2 alerts could not be acknowledged when the incident was declared from them.",
|
|
1765
|
+
{
|
|
1766
|
+
projectId: PROJECT_ID.toString(),
|
|
1767
|
+
incidentId: INCIDENT_ID.toString(),
|
|
1768
|
+
},
|
|
1769
|
+
);
|
|
1770
|
+
expect(acknowledgementErrorLogs()).toHaveLength(1);
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
test("alerts left as they were (already acknowledged, or moved by the linked-alert sync) are not failures", async () => {
|
|
1774
|
+
acknowledge.mockResolvedValue({
|
|
1775
|
+
acknowledgedAlertIds: [],
|
|
1776
|
+
alreadyAcknowledgedAlertIds: [new ObjectID(ALERT_ID)],
|
|
1777
|
+
leftToLinkedAlertSyncAlertIds: [new ObjectID(ALERT_ID_2)],
|
|
1778
|
+
failed: [],
|
|
1779
|
+
} as never);
|
|
1780
|
+
|
|
1781
|
+
await onCreateSuccess(carried(alertIds), createdIncident(USER_ID));
|
|
1782
|
+
await settle();
|
|
1783
|
+
|
|
1784
|
+
expect(acknowledgementErrorLogs()).toEqual([]);
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
describe("nothing is acknowledged unless the declaration asked for it, and left something to acknowledge", () => {
|
|
1789
|
+
test.each([
|
|
1790
|
+
[
|
|
1791
|
+
"a carry-forward from before the switch existed",
|
|
1792
|
+
(): unknown => {
|
|
1793
|
+
return { alertIdsToLink: alertIds };
|
|
1794
|
+
},
|
|
1795
|
+
],
|
|
1796
|
+
[
|
|
1797
|
+
"a carry-forward with a state but no alerts to acknowledge (from before they were carried)",
|
|
1798
|
+
(): unknown => {
|
|
1799
|
+
return {
|
|
1800
|
+
alertIdsToLink: alertIds,
|
|
1801
|
+
acknowledgedAlertStateId: new ObjectID(ACKNOWLEDGED_ALERT_STATE_ID),
|
|
1802
|
+
};
|
|
1803
|
+
},
|
|
1804
|
+
],
|
|
1805
|
+
[
|
|
1806
|
+
"a null acknowledged alert state, even with alerts to acknowledge",
|
|
1807
|
+
(): unknown => {
|
|
1808
|
+
return carried(alertIds, alertIds, null);
|
|
1809
|
+
},
|
|
1810
|
+
],
|
|
1811
|
+
[
|
|
1812
|
+
"an undefined acknowledged alert state, even with alerts to acknowledge",
|
|
1813
|
+
(): unknown => {
|
|
1814
|
+
return {
|
|
1815
|
+
alertIdsToLink: alertIds,
|
|
1816
|
+
acknowledgedAlertStateId: undefined,
|
|
1817
|
+
alertIdsToAcknowledge: alertIds,
|
|
1818
|
+
};
|
|
1819
|
+
},
|
|
1820
|
+
],
|
|
1821
|
+
[
|
|
1822
|
+
"an acknowledged alert state and every alert already acknowledged (none to acknowledge)",
|
|
1823
|
+
(): unknown => {
|
|
1824
|
+
return carried(alertIds, []);
|
|
1825
|
+
},
|
|
1826
|
+
],
|
|
1827
|
+
])(
|
|
1828
|
+
"%s links the alerts and acknowledges none",
|
|
1829
|
+
async (_label: string, carryForward: () => unknown) => {
|
|
1830
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1831
|
+
|
|
1832
|
+
await expect(onCreateSuccess(carryForward(), incident)).resolves.toBe(
|
|
1833
|
+
incident,
|
|
1834
|
+
);
|
|
1835
|
+
await settle();
|
|
1836
|
+
|
|
1837
|
+
expect(link).toHaveBeenCalledTimes(1);
|
|
1838
|
+
expect(ids((link.mock.calls[0]![0] as LinkArgs).alertIds)).toEqual([
|
|
1839
|
+
ALERT_ID,
|
|
1840
|
+
ALERT_ID_2,
|
|
1841
|
+
]);
|
|
1842
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1843
|
+
expect(acknowledgementErrorLogs()).toEqual([]);
|
|
1844
|
+
},
|
|
1845
|
+
);
|
|
1846
|
+
|
|
1847
|
+
test("linking that throws without the request acknowledges none", async () => {
|
|
1848
|
+
link.mockRejectedValue(new Error("database unavailable") as never);
|
|
1849
|
+
|
|
1850
|
+
await onCreateSuccess(
|
|
1851
|
+
carried(alertIds, alertIds, null),
|
|
1852
|
+
createdIncident(USER_ID),
|
|
1853
|
+
);
|
|
1854
|
+
await settle();
|
|
1855
|
+
|
|
1856
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
test.each([
|
|
1860
|
+
["no carry-forward", null],
|
|
1861
|
+
["an undefined carry-forward", undefined],
|
|
1862
|
+
[
|
|
1863
|
+
"an empty list of alerts, even with an acknowledged state",
|
|
1864
|
+
{
|
|
1865
|
+
alertIdsToLink: [],
|
|
1866
|
+
acknowledgedAlertStateId: new ObjectID(ACKNOWLEDGED_ALERT_STATE_ID),
|
|
1867
|
+
alertIdsToAcknowledge: [],
|
|
1868
|
+
},
|
|
1869
|
+
],
|
|
1870
|
+
[
|
|
1871
|
+
"no alerts to link, even with alerts to acknowledge",
|
|
1872
|
+
{
|
|
1873
|
+
alertIdsToLink: [],
|
|
1874
|
+
acknowledgedAlertStateId: new ObjectID(ACKNOWLEDGED_ALERT_STATE_ID),
|
|
1875
|
+
alertIdsToAcknowledge: [new ObjectID(ALERT_ID)],
|
|
1876
|
+
},
|
|
1877
|
+
],
|
|
1878
|
+
])(
|
|
1879
|
+
"an incident declared with %s acknowledges nothing",
|
|
1880
|
+
async (_label: string, carryForward: unknown) => {
|
|
1881
|
+
const incident: Incident = createdIncident(USER_ID);
|
|
1882
|
+
|
|
1883
|
+
await expect(onCreateSuccess(carryForward, incident)).resolves.toBe(
|
|
1884
|
+
incident,
|
|
1885
|
+
);
|
|
1886
|
+
await settle();
|
|
1887
|
+
|
|
1888
|
+
expect(link).not.toHaveBeenCalled();
|
|
1889
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1890
|
+
},
|
|
1891
|
+
);
|
|
1892
|
+
});
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
describe("declaring and acknowledging in one request, from onBeforeCreate to onCreateSuccess", () => {
|
|
1896
|
+
let counter: jest.SpyInstance;
|
|
1897
|
+
let authorize: jest.SpyInstance;
|
|
1898
|
+
let link: jest.SpyInstance;
|
|
1899
|
+
let acknowledge: jest.SpyInstance;
|
|
1900
|
+
|
|
1901
|
+
beforeEach(() => {
|
|
1902
|
+
counter = stubBeforeCreate();
|
|
1903
|
+
stubCreateSuccessChain();
|
|
1904
|
+
|
|
1905
|
+
jest
|
|
1906
|
+
.spyOn(AlertStateService, "findOneBy")
|
|
1907
|
+
.mockResolvedValue(acknowledgedAlertState() as never);
|
|
1908
|
+
authorize = jest
|
|
1909
|
+
.spyOn(AlertStateChangeAuthorization, "assertCanChangeStateOfAlerts")
|
|
1910
|
+
.mockResolvedValue(undefined as never);
|
|
1911
|
+
|
|
1912
|
+
link = stubLinking();
|
|
1913
|
+
acknowledge = jest
|
|
1914
|
+
.spyOn(IncidentAlertService, "acknowledgeAlertsDeclaredWithIncident")
|
|
1915
|
+
.mockResolvedValue(emptyAcknowledgeResult() as never);
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
// The real AlertStateChangeAuthorization, watched.
|
|
1919
|
+
function useRealPermissionCheck(): void {
|
|
1920
|
+
authorize.mockRestore();
|
|
1921
|
+
authorize = jest.spyOn(
|
|
1922
|
+
AlertStateChangeAuthorization,
|
|
1923
|
+
"assertCanChangeStateOfAlerts",
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
type Declared = {
|
|
1928
|
+
carriedForward: CarriedForward | null;
|
|
1929
|
+
incident: Incident;
|
|
1930
|
+
};
|
|
1931
|
+
|
|
1932
|
+
async function declare(
|
|
1933
|
+
miscDataProps: JSONObject,
|
|
1934
|
+
props: DatabaseCommonInteractionProps,
|
|
1935
|
+
): Promise<Declared> {
|
|
1936
|
+
const before: OnCreate<Incident> = await onBeforeCreate(
|
|
1937
|
+
miscDataProps,
|
|
1938
|
+
props,
|
|
1939
|
+
);
|
|
1940
|
+
|
|
1941
|
+
// The row as saved.
|
|
1942
|
+
const created: Incident = before.createBy.data;
|
|
1943
|
+
created._id = INCIDENT_ID.toString();
|
|
1944
|
+
|
|
1945
|
+
const incident: Incident = await callHook<Incident>(
|
|
1946
|
+
IncidentService,
|
|
1947
|
+
"onCreateSuccess",
|
|
1948
|
+
{
|
|
1949
|
+
createBy: before.createBy,
|
|
1950
|
+
carryForward: before.carryForward,
|
|
1951
|
+
},
|
|
1952
|
+
created,
|
|
1953
|
+
);
|
|
1954
|
+
|
|
1955
|
+
return {
|
|
1956
|
+
carriedForward: (before.carryForward as CarriedForward) || null,
|
|
1957
|
+
incident: incident,
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
test("a user who ticks the box has the validated alerts linked and then acknowledged as them", async () => {
|
|
1962
|
+
await declare(
|
|
1963
|
+
{
|
|
1964
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [
|
|
1965
|
+
ALERT_ID,
|
|
1966
|
+
ALERT_ID_2,
|
|
1967
|
+
ALERT_ID.toUpperCase(),
|
|
1968
|
+
],
|
|
1969
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
1970
|
+
},
|
|
1971
|
+
userProps(Permission.ProjectMember),
|
|
1972
|
+
);
|
|
1973
|
+
|
|
1974
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
1975
|
+
|
|
1976
|
+
const args: AcknowledgeArgs = acknowledge.mock
|
|
1977
|
+
.calls[0]![0] as AcknowledgeArgs;
|
|
1978
|
+
|
|
1979
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
1980
|
+
expect(args.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
1981
|
+
expect(args.incidentId.toString()).toBe(INCIDENT_ID.toString());
|
|
1982
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1983
|
+
expect(ids(args.linkedAlertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
1984
|
+
expect(args.acknowledgedByUserId).toBe(USER_ID);
|
|
1985
|
+
|
|
1986
|
+
expect(link.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
1987
|
+
acknowledge.mock.invocationCallOrder[0]!,
|
|
1988
|
+
);
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
test("a user who leaves the box unticked has the alerts linked and not acknowledged", async () => {
|
|
1992
|
+
await declare(
|
|
1993
|
+
{ [INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2] },
|
|
1994
|
+
userProps(Permission.ProjectMember),
|
|
1995
|
+
);
|
|
1996
|
+
|
|
1997
|
+
expect(link).toHaveBeenCalledTimes(1);
|
|
1998
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
test("an API key that asks for it has the alerts acknowledged, credited to nobody", async () => {
|
|
2002
|
+
await declare(
|
|
2003
|
+
{
|
|
2004
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID],
|
|
2005
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
2006
|
+
},
|
|
2007
|
+
apiKeyProps(),
|
|
2008
|
+
);
|
|
2009
|
+
|
|
2010
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
2011
|
+
|
|
2012
|
+
const args: AcknowledgeArgs = acknowledge.mock
|
|
2013
|
+
.calls[0]![0] as AcknowledgeArgs;
|
|
2014
|
+
|
|
2015
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID]);
|
|
2016
|
+
expect(args.acknowledgedByUserId).toBeUndefined();
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
describe("with the real permission check, a responder who may change only the alerts of one label", () => {
|
|
2020
|
+
let teamA: Label;
|
|
2021
|
+
let teamB: Label;
|
|
2022
|
+
|
|
2023
|
+
beforeEach(() => {
|
|
2024
|
+
teamA = createLabel("team-a");
|
|
2025
|
+
teamB = createLabel("team-b");
|
|
2026
|
+
useRealPermissionCheck();
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
test("declares from an open team-a alert and a resolved team-b alert: both linked, only the open one checked and acknowledged", async () => {
|
|
2030
|
+
storeAlert(ALERT_ID, {
|
|
2031
|
+
stateOrder: CREATED_ALERT_STATE_ORDER,
|
|
2032
|
+
labels: [teamA],
|
|
2033
|
+
});
|
|
2034
|
+
storeAlert(ALERT_ID_2, {
|
|
2035
|
+
stateOrder: RESOLVED_ALERT_STATE_ORDER,
|
|
2036
|
+
labels: [teamB],
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
const props: DatabaseCommonInteractionProps = labelScopedProps(teamA);
|
|
2040
|
+
|
|
2041
|
+
// The responder really may not change the resolved team-b alert.
|
|
2042
|
+
const both: Promise<void> =
|
|
2043
|
+
AlertStateChangeAuthorization.assertCanChangeStateOfAlerts({
|
|
2044
|
+
projectId: PROJECT_ID,
|
|
2045
|
+
alertIds: [new ObjectID(ALERT_ID), new ObjectID(ALERT_ID_2)],
|
|
2046
|
+
props: props,
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
await expect(both).rejects.toBeInstanceOf(NotAuthorizedException);
|
|
2050
|
+
await expect(both).rejects.toThrow(
|
|
2051
|
+
"You do not have permission to update this Alert. You need to have one of the following labels: team-b.",
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
authorize.mockClear();
|
|
2055
|
+
findAlerts.mockClear();
|
|
2056
|
+
|
|
2057
|
+
const declared: Declared = await declare(
|
|
2058
|
+
{
|
|
2059
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
2060
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
2061
|
+
},
|
|
2062
|
+
props,
|
|
2063
|
+
);
|
|
2064
|
+
|
|
2065
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
2066
|
+
|
|
2067
|
+
// Only the alert that will be written was checked, and it passed.
|
|
2068
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
2069
|
+
expect(
|
|
2070
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
2071
|
+
).toEqual([ALERT_ID]);
|
|
2072
|
+
await expect(authorize.mock.results[0]!.value).resolves.toBeUndefined();
|
|
2073
|
+
|
|
2074
|
+
// ...through the responder's team-a scope, for that alert alone.
|
|
2075
|
+
const scopedReads: Array<FindAlertsArgs> = findAlerts.mock.calls
|
|
2076
|
+
.map((call: Array<unknown>): FindAlertsArgs => {
|
|
2077
|
+
return call[0] as FindAlertsArgs;
|
|
2078
|
+
})
|
|
2079
|
+
.filter((args: FindAlertsArgs): boolean => {
|
|
2080
|
+
return args.query["labels"] !== undefined;
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
expect(scopedReads).toHaveLength(1);
|
|
2084
|
+
expect(
|
|
2085
|
+
rawValues((scopedReads[0]!.query["labels"] as { _id: unknown })._id),
|
|
2086
|
+
).toEqual([teamA.id!.toString()]);
|
|
2087
|
+
expect(rawValues(scopedReads[0]!.query["_id"])).toEqual([ALERT_ID]);
|
|
2088
|
+
|
|
2089
|
+
expect(ids(declared.carriedForward!.alertIdsToLink)).toEqual([
|
|
2090
|
+
ALERT_ID,
|
|
2091
|
+
ALERT_ID_2,
|
|
2092
|
+
]);
|
|
2093
|
+
expect(ids(declared.carriedForward!.alertIdsToAcknowledge)).toEqual([
|
|
2094
|
+
ALERT_ID,
|
|
2095
|
+
]);
|
|
2096
|
+
|
|
2097
|
+
// Both linked, as the responder...
|
|
2098
|
+
expect(link).toHaveBeenCalledTimes(1);
|
|
2099
|
+
expect(ids((link.mock.calls[0]![0] as LinkArgs).alertIds)).toEqual([
|
|
2100
|
+
ALERT_ID,
|
|
2101
|
+
ALERT_ID_2,
|
|
2102
|
+
]);
|
|
2103
|
+
|
|
2104
|
+
// ...and only the open alert acknowledged, as them.
|
|
2105
|
+
expect(acknowledge).toHaveBeenCalledTimes(1);
|
|
2106
|
+
|
|
2107
|
+
const args: AcknowledgeArgs = acknowledge.mock
|
|
2108
|
+
.calls[0]![0] as AcknowledgeArgs;
|
|
2109
|
+
|
|
2110
|
+
expect(ids(args.alertIds)).toEqual([ALERT_ID]);
|
|
2111
|
+
expect(ids(args.linkedAlertIds)).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
2112
|
+
expect(args.acknowledgedByUserId).toBe(USER_ID);
|
|
2113
|
+
expect(link.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
2114
|
+
acknowledge.mock.invocationCallOrder[0]!,
|
|
2115
|
+
);
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
test("is refused while the team-b alert is still open, before the number is taken and before anything is linked", async () => {
|
|
2119
|
+
storeAlert(ALERT_ID, {
|
|
2120
|
+
stateOrder: CREATED_ALERT_STATE_ORDER,
|
|
2121
|
+
labels: [teamA],
|
|
2122
|
+
});
|
|
2123
|
+
storeAlert(ALERT_ID_2, {
|
|
2124
|
+
stateOrder: CREATED_ALERT_STATE_ORDER,
|
|
2125
|
+
labels: [teamB],
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
const declared: Promise<Declared> = declare(
|
|
2129
|
+
{
|
|
2130
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
2131
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
2132
|
+
},
|
|
2133
|
+
labelScopedProps(teamA),
|
|
2134
|
+
);
|
|
2135
|
+
|
|
2136
|
+
await expect(declared).rejects.toBeInstanceOf(BadDataException);
|
|
2137
|
+
await expect(declared).rejects.toThrow(MAY_NOT_ACKNOWLEDGE_MESSAGE);
|
|
2138
|
+
|
|
2139
|
+
expect(authorize).toHaveBeenCalledTimes(1);
|
|
2140
|
+
expect(
|
|
2141
|
+
ids((authorize.mock.calls[0]![0] as AuthorizeArgs).alertIds),
|
|
2142
|
+
).toEqual([ALERT_ID, ALERT_ID_2]);
|
|
2143
|
+
expect(counter).not.toHaveBeenCalled();
|
|
2144
|
+
expect(link).not.toHaveBeenCalled();
|
|
2145
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
2146
|
+
});
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
test("every alert already acknowledged or resolved: the key is accepted, nothing is checked and nothing is acknowledged", async () => {
|
|
2150
|
+
storeAlert(ALERT_ID, { stateOrder: ACKNOWLEDGED_ALERT_STATE_ORDER });
|
|
2151
|
+
storeAlert(ALERT_ID_2, { stateOrder: RESOLVED_ALERT_STATE_ORDER });
|
|
2152
|
+
|
|
2153
|
+
/*
|
|
2154
|
+
* The real check, for a caller it would refuse (an IncidentMember may
|
|
2155
|
+
* link alerts but not change their states): it must not be asked.
|
|
2156
|
+
*/
|
|
2157
|
+
useRealPermissionCheck();
|
|
2158
|
+
|
|
2159
|
+
const declared: Declared = await declare(
|
|
2160
|
+
{
|
|
2161
|
+
[INCIDENT_ALERT_IDS_TO_LINK_KEY]: [ALERT_ID, ALERT_ID_2],
|
|
2162
|
+
[INCIDENT_ACKNOWLEDGE_ALERTS_TO_LINK_KEY]: true,
|
|
2163
|
+
},
|
|
2164
|
+
userProps(Permission.IncidentMember),
|
|
2165
|
+
);
|
|
2166
|
+
await settle();
|
|
2167
|
+
|
|
2168
|
+
expect(declared.incident._id).toBe(INCIDENT_ID.toString());
|
|
2169
|
+
expect(counter).toHaveBeenCalledTimes(1);
|
|
2170
|
+
expect(authorize).not.toHaveBeenCalled();
|
|
2171
|
+
|
|
2172
|
+
expect(ids(declared.carriedForward!.alertIdsToLink)).toEqual([
|
|
2173
|
+
ALERT_ID,
|
|
2174
|
+
ALERT_ID_2,
|
|
2175
|
+
]);
|
|
2176
|
+
expect(declared.carriedForward!.acknowledgedAlertStateId!.toString()).toBe(
|
|
2177
|
+
ACKNOWLEDGED_ALERT_STATE_ID,
|
|
2178
|
+
);
|
|
2179
|
+
expect(declared.carriedForward!.alertIdsToAcknowledge).toEqual([]);
|
|
2180
|
+
|
|
2181
|
+
expect(link).toHaveBeenCalledTimes(1);
|
|
2182
|
+
expect(ids((link.mock.calls[0]![0] as LinkArgs).alertIds)).toEqual([
|
|
2183
|
+
ALERT_ID,
|
|
2184
|
+
ALERT_ID_2,
|
|
2185
|
+
]);
|
|
2186
|
+
expect(acknowledge).not.toHaveBeenCalled();
|
|
2187
|
+
expect(acknowledgementErrorLogs()).toEqual([]);
|
|
2188
|
+
});
|
|
2189
|
+
});
|