@oneuptime/common 14.0.2 → 14.0.3
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/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { expect, jest } from "@jest/globals";
|
|
2
|
+
import { act, fireEvent } from "@testing-library/react";
|
|
3
|
+
import VMwareResourceModel from "../../../Models/DatabaseModels/VMwareResource";
|
|
4
|
+
import VMwareVCenter from "../../../Models/DatabaseModels/VMwareVCenter";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Helpers for the VMware page render tests (VMwareOverviewTooltips,
|
|
8
|
+
* VMwareResourcePagesTooltips). Not a test file itself (no .test. in the
|
|
9
|
+
* name), so jest does not run it.
|
|
10
|
+
*
|
|
11
|
+
* An (i) is an InfoTooltip: a <button aria-label="About <title>"> whose
|
|
12
|
+
* Tippy popup is created lazily on first hover and portalled to <body>.
|
|
13
|
+
* Under jsdom a hidden popup never finishes animating out, so a page that
|
|
14
|
+
* hovers several (i)s holds several popups at once; the text is therefore
|
|
15
|
+
* read through the aria-describedby link Tippy puts on the trigger while it
|
|
16
|
+
* is shown, never through "the" tooltip on the page.
|
|
17
|
+
*
|
|
18
|
+
* Every helper that waits expects jest fake timers.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ABOUT: string = "About ";
|
|
22
|
+
|
|
23
|
+
export function infoButtons(
|
|
24
|
+
root: ParentNode = document.body,
|
|
25
|
+
): Array<HTMLElement> {
|
|
26
|
+
return Array.from(
|
|
27
|
+
root.querySelectorAll<HTMLElement>(`button[aria-label^="${ABOUT}"]`),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// What each (i) under `root` explains, in document order.
|
|
32
|
+
export function infoLabels(root: ParentNode = document.body): Array<string> {
|
|
33
|
+
return infoButtons(root).map((button: HTMLElement): string => {
|
|
34
|
+
return (button.getAttribute("aria-label") || "").slice(ABOUT.length);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function infoButtonsFor(
|
|
39
|
+
label: string,
|
|
40
|
+
root: ParentNode = document.body,
|
|
41
|
+
): Array<HTMLElement> {
|
|
42
|
+
return infoButtons(root).filter((button: HTMLElement): boolean => {
|
|
43
|
+
return button.getAttribute("aria-label") === `${ABOUT}${label}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Hover an (i), read what its tooltip says, and move the pointer away.
|
|
48
|
+
export async function explanationOnHover(button: HTMLElement): Promise<string> {
|
|
49
|
+
fireEvent.mouseEnter(button);
|
|
50
|
+
await act(async () => {
|
|
51
|
+
jest.advanceTimersByTime(250);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const describedBy: string | null = button.getAttribute("aria-describedby");
|
|
55
|
+
const popup: HTMLElement | null = describedBy
|
|
56
|
+
? document.getElementById(describedBy)
|
|
57
|
+
: null;
|
|
58
|
+
const text: string = popup?.textContent || "";
|
|
59
|
+
|
|
60
|
+
fireEvent.mouseLeave(button);
|
|
61
|
+
await act(async () => {
|
|
62
|
+
jest.advanceTimersByTime(250);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The same, reached with the keyboard.
|
|
69
|
+
export async function explanationOnFocus(button: HTMLElement): Promise<string> {
|
|
70
|
+
act(() => {
|
|
71
|
+
button.focus();
|
|
72
|
+
});
|
|
73
|
+
await act(async () => {
|
|
74
|
+
jest.advanceTimersByTime(250);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const describedBy: string | null = button.getAttribute("aria-describedby");
|
|
78
|
+
const text: string = describedBy
|
|
79
|
+
? document.getElementById(describedBy)?.textContent || ""
|
|
80
|
+
: "";
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
button.blur();
|
|
84
|
+
});
|
|
85
|
+
await act(async () => {
|
|
86
|
+
jest.advanceTimersByTime(250);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Hover every (i) in turn and pair each label with what it says, in
|
|
94
|
+
* document order.
|
|
95
|
+
*/
|
|
96
|
+
export async function explainedLabels(
|
|
97
|
+
root: ParentNode = document.body,
|
|
98
|
+
): Promise<Array<[string, string]>> {
|
|
99
|
+
const result: Array<[string, string]> = [];
|
|
100
|
+
|
|
101
|
+
for (const button of infoButtons(root)) {
|
|
102
|
+
result.push([
|
|
103
|
+
(button.getAttribute("aria-label") || "").slice(ABOUT.length),
|
|
104
|
+
await explanationOnHover(button),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* An (i) is a button of its own: never inside another button or a link,
|
|
113
|
+
* where a click on it would also press or follow its container.
|
|
114
|
+
*/
|
|
115
|
+
export function expectNotNested(button: HTMLElement): void {
|
|
116
|
+
expect(button.tagName).toBe("BUTTON");
|
|
117
|
+
expect(button.getAttribute("type")).toBe("button");
|
|
118
|
+
expect(button.parentElement?.closest("button, a")).toBeNull();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function inventoryRow(
|
|
122
|
+
fields: Partial<Record<keyof VMwareResourceModel, unknown>>,
|
|
123
|
+
): VMwareResourceModel {
|
|
124
|
+
const row: VMwareResourceModel = new VMwareResourceModel();
|
|
125
|
+
|
|
126
|
+
Object.assign(row, fields);
|
|
127
|
+
|
|
128
|
+
return row;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function vcenterModel(
|
|
132
|
+
fields: Partial<Record<keyof VMwareVCenter, unknown>> = {},
|
|
133
|
+
): VMwareVCenter {
|
|
134
|
+
const vcenter: VMwareVCenter = new VMwareVCenter();
|
|
135
|
+
|
|
136
|
+
Object.assign(vcenter, {
|
|
137
|
+
name: "prod-vcenter",
|
|
138
|
+
otelCollectorStatus: "connected",
|
|
139
|
+
...fields,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return vcenter;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const GIB: number = 1024 * 1024 * 1024;
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import Models from "../../../Models/DatabaseModels/Index";
|
|
2
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
3
|
+
import { AlertFeedEventType } from "../../../Models/DatabaseModels/AlertFeed";
|
|
4
|
+
import AlertReminderRule from "../../../Models/DatabaseModels/AlertReminderRule";
|
|
5
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
6
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
7
|
+
import IncidentAlert from "../../../Models/DatabaseModels/IncidentAlert";
|
|
8
|
+
import { IncidentFeedEventType } from "../../../Models/DatabaseModels/IncidentFeed";
|
|
9
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
10
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
11
|
+
import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
|
|
12
|
+
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
|
13
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
14
|
+
import { OwnedThroughMetadata } from "../../../Types/Database/AccessControl/OwnedThrough";
|
|
15
|
+
import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
|
|
16
|
+
import TableColumnType from "../../../Types/Database/TableColumnType";
|
|
17
|
+
import { UniqueColumnsTogetherMetadata } from "../../../Types/Database/UniqueColumnsTogether";
|
|
18
|
+
import { INCIDENT_ALERT_ALREADY_LINKED_MESSAGE } from "../../../Types/Incident/IncidentAlertLink";
|
|
19
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
20
|
+
import ReminderStopState from "../../../Types/Reminder/ReminderStopState";
|
|
21
|
+
import Permission, {
|
|
22
|
+
PermissionGroup,
|
|
23
|
+
PermissionHelper,
|
|
24
|
+
PermissionProps,
|
|
25
|
+
UserTenantAccessPermission,
|
|
26
|
+
} from "../../../Types/Permission";
|
|
27
|
+
import UserType from "../../../Types/UserType";
|
|
28
|
+
import { getFeedEventTypeLabel } from "../../../UI/Components/Feed/FeedOptions";
|
|
29
|
+
import { describe, expect, test } from "@jest/globals";
|
|
30
|
+
import { getMetadataArgsStorage } from "typeorm";
|
|
31
|
+
import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
|
|
32
|
+
import type { RelationMetadataArgs } from "typeorm/metadata-args/RelationMetadataArgs";
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* IncidentAlert: an alert linked to an incident, at the schema and
|
|
36
|
+
* permission level.
|
|
37
|
+
*
|
|
38
|
+
* The service tests pass just as happily against a link table the wrong role
|
|
39
|
+
* can write, whose pair is not unique, that forgets the incident's label
|
|
40
|
+
* scope, or that an edit can re-point at another alert. The dashboard pages,
|
|
41
|
+
* the migration and the docs all name what is pinned here, so a change to
|
|
42
|
+
* the model fails here first.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
type ModelType = { new (): BaseModel };
|
|
46
|
+
|
|
47
|
+
const MODEL_TYPES: Array<ModelType> = Models as Array<ModelType>;
|
|
48
|
+
const PERMISSION_PROPS: Array<PermissionProps> =
|
|
49
|
+
PermissionHelper.getAllPermissionProps();
|
|
50
|
+
|
|
51
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
52
|
+
"0194d4ba-0000-4000-8000-000000000001",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const WRITERS: Array<Permission> = [
|
|
56
|
+
Permission.ProjectOwner,
|
|
57
|
+
Permission.ProjectAdmin,
|
|
58
|
+
Permission.ProjectMember,
|
|
59
|
+
Permission.IncidentAdmin,
|
|
60
|
+
Permission.IncidentMember,
|
|
61
|
+
Permission.AlertAdmin,
|
|
62
|
+
Permission.AlertMember,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const READERS: Array<Permission> = [
|
|
66
|
+
Permission.ProjectOwner,
|
|
67
|
+
Permission.ProjectAdmin,
|
|
68
|
+
Permission.ProjectMember,
|
|
69
|
+
Permission.Viewer,
|
|
70
|
+
Permission.IncidentAdmin,
|
|
71
|
+
Permission.IncidentMember,
|
|
72
|
+
Permission.IncidentViewer,
|
|
73
|
+
Permission.AlertAdmin,
|
|
74
|
+
Permission.AlertMember,
|
|
75
|
+
Permission.AlertViewer,
|
|
76
|
+
Permission.ReadIncidentAlert,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const GRANULAR_PERMISSIONS: Array<Permission> = [
|
|
80
|
+
Permission.CreateIncidentAlert,
|
|
81
|
+
Permission.DeleteIncidentAlert,
|
|
82
|
+
Permission.EditIncidentAlert,
|
|
83
|
+
Permission.ReadIncidentAlert,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function model(): IncidentAlert {
|
|
87
|
+
return new IncidentAlert();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function propsWith(permission: Permission): DatabaseCommonInteractionProps {
|
|
91
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
92
|
+
projectId: PROJECT_ID,
|
|
93
|
+
_type: "UserTenantAccessPermission",
|
|
94
|
+
permissions: [
|
|
95
|
+
{
|
|
96
|
+
_type: "UserPermission",
|
|
97
|
+
permission: permission,
|
|
98
|
+
labelIds: [],
|
|
99
|
+
isBlockPermission: false,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
tenantId: PROJECT_ID,
|
|
106
|
+
userId: ObjectID.generate(),
|
|
107
|
+
userType: UserType.User,
|
|
108
|
+
userTenantAccessPermission: {
|
|
109
|
+
[PROJECT_ID.toString()]: tenantPermission,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function linkPayload(): IncidentAlert {
|
|
115
|
+
const link: IncidentAlert = new IncidentAlert();
|
|
116
|
+
link.projectId = PROJECT_ID;
|
|
117
|
+
link.incidentId = ObjectID.generate();
|
|
118
|
+
link.alertId = ObjectID.generate();
|
|
119
|
+
return link;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("IncidentAlert registration", () => {
|
|
123
|
+
test("is a registered database model", () => {
|
|
124
|
+
expect(MODEL_TYPES).toContain(IncidentAlert);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("owns its table name and CRUD route", () => {
|
|
128
|
+
expect(model().tableName).toBe("IncidentAlert");
|
|
129
|
+
expect(model().getCrudApiPath()?.toString()).toBe("/incident-alert");
|
|
130
|
+
|
|
131
|
+
const routeClaimants: Array<string> = MODEL_TYPES.filter(
|
|
132
|
+
(modelType: ModelType) => {
|
|
133
|
+
return (
|
|
134
|
+
new modelType().getCrudApiPath()?.toString() === "/incident-alert"
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
).map((modelType: ModelType) => {
|
|
138
|
+
return modelType.name;
|
|
139
|
+
});
|
|
140
|
+
const tableClaimants: Array<string> = MODEL_TYPES.filter(
|
|
141
|
+
(modelType: ModelType) => {
|
|
142
|
+
return new modelType().tableName === "IncidentAlert";
|
|
143
|
+
},
|
|
144
|
+
).map((modelType: ModelType) => {
|
|
145
|
+
return modelType.name;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// A duplicate route silently shadows another model's API.
|
|
149
|
+
expect(routeClaimants).toEqual(["IncidentAlert"]);
|
|
150
|
+
expect(tableClaimants).toEqual(["IncidentAlert"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("is named and documented for the API reference", () => {
|
|
154
|
+
expect(model().singularName).toBe("Incident Alert");
|
|
155
|
+
expect(model().pluralName).toBe("Incident Alerts");
|
|
156
|
+
expect(model().tableDescription?.length).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("is scoped to a project through projectId", () => {
|
|
160
|
+
expect(model().getTenantColumn()).toBe("projectId");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("is documented, exposed to MCP, and offered to workflows", () => {
|
|
164
|
+
expect(model().enableDocumentation).toBe(true);
|
|
165
|
+
expect(model().enableMCP).toBe(true);
|
|
166
|
+
expect(model().enableWorkflowOn).toEqual({
|
|
167
|
+
create: true,
|
|
168
|
+
delete: true,
|
|
169
|
+
update: true,
|
|
170
|
+
read: true,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("IncidentAlert table permissions", () => {
|
|
176
|
+
test("create, delete and update: the incident and alert writers", () => {
|
|
177
|
+
expect(model().getCreatePermissions()).toEqual([
|
|
178
|
+
...WRITERS,
|
|
179
|
+
Permission.CreateIncidentAlert,
|
|
180
|
+
]);
|
|
181
|
+
expect(model().getDeletePermissions()).toEqual([
|
|
182
|
+
...WRITERS,
|
|
183
|
+
Permission.DeleteIncidentAlert,
|
|
184
|
+
]);
|
|
185
|
+
expect(model().getUpdatePermissions()).toEqual([
|
|
186
|
+
...WRITERS,
|
|
187
|
+
Permission.EditIncidentAlert,
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("read: the incident and alert readers", () => {
|
|
192
|
+
expect(model().getReadPermissions()).toEqual(READERS);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("no list names ProjectUser", () => {
|
|
196
|
+
for (const list of [
|
|
197
|
+
model().getCreatePermissions(),
|
|
198
|
+
model().getReadPermissions(),
|
|
199
|
+
model().getUpdatePermissions(),
|
|
200
|
+
model().getDeletePermissions(),
|
|
201
|
+
]) {
|
|
202
|
+
expect(list).not.toContain(Permission.ProjectUser);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test.each(GRANULAR_PERMISSIONS)(
|
|
207
|
+
"%s is a tenant-assignable Incident permission, not a label permission",
|
|
208
|
+
(permission: Permission) => {
|
|
209
|
+
const props: PermissionProps | undefined = PERMISSION_PROPS.find(
|
|
210
|
+
(candidate: PermissionProps) => {
|
|
211
|
+
return candidate.permission === permission;
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(props).toBeDefined();
|
|
216
|
+
expect(props!.group).toBe(PermissionGroup.Incident);
|
|
217
|
+
expect(props!.isAssignableToTenant).toBe(true);
|
|
218
|
+
expect(props!.isAccessControlPermission).toBe(false);
|
|
219
|
+
expect(props!.title.length).toBeGreaterThan(0);
|
|
220
|
+
expect(props!.description.length).toBeGreaterThan(0);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
test("every permission the model names is in the permission catalogue", () => {
|
|
225
|
+
const named: Set<Permission> = new Set([
|
|
226
|
+
...model().getCreatePermissions(),
|
|
227
|
+
...model().getReadPermissions(),
|
|
228
|
+
...model().getUpdatePermissions(),
|
|
229
|
+
...model().getDeletePermissions(),
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
for (const access of Object.values(
|
|
233
|
+
model().getColumnAccessControlForAllColumns(),
|
|
234
|
+
)) {
|
|
235
|
+
for (const permission of [
|
|
236
|
+
...access.create,
|
|
237
|
+
...access.read,
|
|
238
|
+
...access.update,
|
|
239
|
+
]) {
|
|
240
|
+
named.add(permission);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const permission of named) {
|
|
245
|
+
expect({
|
|
246
|
+
permission,
|
|
247
|
+
catalogued: PERMISSION_PROPS.some((candidate: PermissionProps) => {
|
|
248
|
+
return candidate.permission === permission;
|
|
249
|
+
}),
|
|
250
|
+
}).toEqual({ permission, catalogued: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test.each([
|
|
255
|
+
Permission.ProjectMember,
|
|
256
|
+
Permission.IncidentMember,
|
|
257
|
+
Permission.AlertMember,
|
|
258
|
+
Permission.ProjectAdmin,
|
|
259
|
+
Permission.CreateIncidentAlert,
|
|
260
|
+
])("%s may create a link", (permission: Permission) => {
|
|
261
|
+
expect(() => {
|
|
262
|
+
ModelPermission.checkCreatePermissions(
|
|
263
|
+
IncidentAlert,
|
|
264
|
+
linkPayload(),
|
|
265
|
+
propsWith(permission),
|
|
266
|
+
);
|
|
267
|
+
}).not.toThrow();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test.each([
|
|
271
|
+
Permission.Viewer,
|
|
272
|
+
Permission.IncidentViewer,
|
|
273
|
+
Permission.AlertViewer,
|
|
274
|
+
Permission.ReadIncidentAlert,
|
|
275
|
+
])("%s may not create a link", (permission: Permission) => {
|
|
276
|
+
expect(() => {
|
|
277
|
+
ModelPermission.checkCreatePermissions(
|
|
278
|
+
IncidentAlert,
|
|
279
|
+
linkPayload(),
|
|
280
|
+
propsWith(permission),
|
|
281
|
+
);
|
|
282
|
+
}).toThrow();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("nobody can set who deleted a link", () => {
|
|
286
|
+
const link: IncidentAlert = linkPayload();
|
|
287
|
+
link.deletedByUserId = ObjectID.generate();
|
|
288
|
+
|
|
289
|
+
expect(() => {
|
|
290
|
+
ModelPermission.checkCreatePermissions(
|
|
291
|
+
IncidentAlert,
|
|
292
|
+
link,
|
|
293
|
+
propsWith(Permission.ProjectAdmin),
|
|
294
|
+
);
|
|
295
|
+
}).toThrow("deletedByUserId");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("IncidentAlert columns", () => {
|
|
300
|
+
const COLUMNS: Array<string> = [
|
|
301
|
+
"project",
|
|
302
|
+
"projectId",
|
|
303
|
+
"incident",
|
|
304
|
+
"incidentId",
|
|
305
|
+
"alert",
|
|
306
|
+
"alertId",
|
|
307
|
+
"createdByUser",
|
|
308
|
+
"createdByUserId",
|
|
309
|
+
"deletedByUser",
|
|
310
|
+
"deletedByUserId",
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
test("has exactly the columns the migration creates", () => {
|
|
314
|
+
const baseColumns: Array<string> = [
|
|
315
|
+
"_id",
|
|
316
|
+
"createdAt",
|
|
317
|
+
"updatedAt",
|
|
318
|
+
"deletedAt",
|
|
319
|
+
"version",
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
expect([...model().getTableColumns().columns].sort()).toEqual(
|
|
323
|
+
[...baseColumns, ...COLUMNS].sort(),
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test.each(COLUMNS)(
|
|
328
|
+
"%s cannot be edited: a link is created or removed, never changed",
|
|
329
|
+
(column: string) => {
|
|
330
|
+
const access: ColumnAccessControl | null =
|
|
331
|
+
model().getColumnAccessControlFor(column);
|
|
332
|
+
|
|
333
|
+
expect(access).not.toBeNull();
|
|
334
|
+
expect(access!.update).toEqual([]);
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
test.each(["projectId", "incidentId", "alertId"])(
|
|
339
|
+
"%s is a required id that relation queries may read",
|
|
340
|
+
(column: string) => {
|
|
341
|
+
const metadata: TableColumnMetadata =
|
|
342
|
+
model().getTableColumnMetadata(column);
|
|
343
|
+
|
|
344
|
+
expect(metadata.type).toBe(TableColumnType.ObjectID);
|
|
345
|
+
expect(metadata.required).toBe(true);
|
|
346
|
+
expect(metadata.canReadOnRelationQuery).toBe(true);
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
test.each(["createdByUserId", "deletedByUserId"])(
|
|
351
|
+
"%s is optional",
|
|
352
|
+
(column: string) => {
|
|
353
|
+
expect(Boolean(model().getTableColumnMetadata(column).required)).toBe(
|
|
354
|
+
false,
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
interface RelationCase {
|
|
360
|
+
relation: string;
|
|
361
|
+
idColumn: string;
|
|
362
|
+
modelType: ModelType;
|
|
363
|
+
onDelete: string;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
test.each([
|
|
367
|
+
{
|
|
368
|
+
relation: "project",
|
|
369
|
+
idColumn: "projectId",
|
|
370
|
+
modelType: Project,
|
|
371
|
+
onDelete: "CASCADE",
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
relation: "incident",
|
|
375
|
+
idColumn: "incidentId",
|
|
376
|
+
modelType: Incident,
|
|
377
|
+
onDelete: "CASCADE",
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
relation: "alert",
|
|
381
|
+
idColumn: "alertId",
|
|
382
|
+
modelType: Alert,
|
|
383
|
+
onDelete: "CASCADE",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
relation: "createdByUser",
|
|
387
|
+
idColumn: "createdByUserId",
|
|
388
|
+
modelType: User,
|
|
389
|
+
onDelete: "SET NULL",
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
relation: "deletedByUser",
|
|
393
|
+
idColumn: "deletedByUserId",
|
|
394
|
+
modelType: User,
|
|
395
|
+
onDelete: "SET NULL",
|
|
396
|
+
},
|
|
397
|
+
] as Array<RelationCase>)(
|
|
398
|
+
"$relation points at its model through $idColumn, and is $onDelete on delete",
|
|
399
|
+
({ relation, idColumn, modelType, onDelete }: RelationCase) => {
|
|
400
|
+
const metadata: TableColumnMetadata =
|
|
401
|
+
model().getTableColumnMetadata(relation);
|
|
402
|
+
|
|
403
|
+
expect(metadata.type).toBe(TableColumnType.Entity);
|
|
404
|
+
expect(metadata.modelType).toBe(modelType);
|
|
405
|
+
expect(metadata.manyToOneRelationColumn).toBe(idColumn);
|
|
406
|
+
|
|
407
|
+
const relationArgs: RelationMetadataArgs | undefined =
|
|
408
|
+
getMetadataArgsStorage().relations.find(
|
|
409
|
+
(candidate: RelationMetadataArgs) => {
|
|
410
|
+
return (
|
|
411
|
+
candidate.target === IncidentAlert &&
|
|
412
|
+
candidate.propertyName === relation
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
expect(relationArgs?.relationType).toBe("many-to-one");
|
|
418
|
+
expect(relationArgs?.options.onDelete).toBe(onDelete);
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
test("the two deletedBy columns cannot be written on create", () => {
|
|
423
|
+
expect(model().getColumnAccessControlFor("deletedByUser")!.create).toEqual(
|
|
424
|
+
[],
|
|
425
|
+
);
|
|
426
|
+
expect(
|
|
427
|
+
model().getColumnAccessControlFor("deletedByUserId")!.create,
|
|
428
|
+
).toEqual([]);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("IncidentAlert uniqueness and scope", () => {
|
|
433
|
+
test("a pair can only be linked once: a unique index backs the service check", () => {
|
|
434
|
+
const indexes: Array<IndexMetadataArgs> =
|
|
435
|
+
getMetadataArgsStorage().indices.filter((index: IndexMetadataArgs) => {
|
|
436
|
+
return index.target === IncidentAlert && Array.isArray(index.columns);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const unique: Array<IndexMetadataArgs> = indexes.filter(
|
|
440
|
+
(index: IndexMetadataArgs) => {
|
|
441
|
+
return index.unique === true;
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
expect(unique).toHaveLength(1);
|
|
446
|
+
expect(unique[0]!.columns).toEqual(["incidentId", "alertId", "projectId"]);
|
|
447
|
+
|
|
448
|
+
const constraints: Array<UniqueColumnsTogetherMetadata> =
|
|
449
|
+
model().getUniqueColumnsTogether();
|
|
450
|
+
|
|
451
|
+
expect(constraints).toEqual([
|
|
452
|
+
{
|
|
453
|
+
columnNames: ["incidentId", "alertId", "projectId"],
|
|
454
|
+
errorMessage: "This alert is already linked to this incident.",
|
|
455
|
+
},
|
|
456
|
+
]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("the unique-together check and the service's unique-index answer share one message", () => {
|
|
460
|
+
/*
|
|
461
|
+
* IncidentAlertService.getException answers a unique-index violation
|
|
462
|
+
* with INCIDENT_ALERT_ALREADY_LINKED_MESSAGE, and the dashboard counts a
|
|
463
|
+
* link as done by that message, so the pre-check must use it too.
|
|
464
|
+
*/
|
|
465
|
+
expect(model().getUniqueColumnsTogether()[0]!.errorMessage).toBe(
|
|
466
|
+
INCIDENT_ALERT_ALREADY_LINKED_MESSAGE,
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("reads follow the incident's labels", () => {
|
|
471
|
+
expect(model().canAccessIfCanReadOn).toBe("incident");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("owners see the links of the incidents they own", () => {
|
|
475
|
+
const ownedThrough: OwnedThroughMetadata | null = model().ownedThrough;
|
|
476
|
+
|
|
477
|
+
expect(ownedThrough?.fkColumn).toBe("incidentId");
|
|
478
|
+
expect(ownedThrough?.parentModels).toEqual([Incident]);
|
|
479
|
+
expect(ownedThrough?.includeProjectScope).toBe(false);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("the project switches for linked alerts", () => {
|
|
484
|
+
test.each([
|
|
485
|
+
"acknowledgeLinkedAlertsWhenIncidentAcknowledged",
|
|
486
|
+
"resolveLinkedAlertsWhenIncidentResolved",
|
|
487
|
+
])(
|
|
488
|
+
"%s is an off-by-default boolean only owners and admins may change",
|
|
489
|
+
(column: string) => {
|
|
490
|
+
const project: Project = new Project();
|
|
491
|
+
const metadata: TableColumnMetadata =
|
|
492
|
+
project.getTableColumnMetadata(column);
|
|
493
|
+
const access: ColumnAccessControl | null =
|
|
494
|
+
project.getColumnAccessControlFor(column);
|
|
495
|
+
|
|
496
|
+
expect(metadata.type).toBe(TableColumnType.Boolean);
|
|
497
|
+
expect(metadata.required).toBe(true);
|
|
498
|
+
expect(metadata.defaultValue).toBe(false);
|
|
499
|
+
expect(project.isDefaultValueColumn(column)).toBe(true);
|
|
500
|
+
expect(access!.create).toEqual([]);
|
|
501
|
+
expect(access!.update).toEqual([
|
|
502
|
+
Permission.ProjectOwner,
|
|
503
|
+
Permission.ProjectAdmin,
|
|
504
|
+
]);
|
|
505
|
+
expect(access!.read).toContain(Permission.ProjectMember);
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
/*
|
|
510
|
+
* Acknowledging an alert always stops its on-call escalation, but its
|
|
511
|
+
* reminders stop on Acknowledged only when its reminder rule says so - and
|
|
512
|
+
* a reminder rule stops on Resolved unless it is changed. The description
|
|
513
|
+
* is published in the API reference, so it must not promise more.
|
|
514
|
+
*/
|
|
515
|
+
test("the acknowledge switch promises reminders stop only when the reminder rule stops on Acknowledged", () => {
|
|
516
|
+
const description: string =
|
|
517
|
+
new Project().getTableColumnMetadata(
|
|
518
|
+
"acknowledgeLinkedAlertsWhenIncidentAcknowledged",
|
|
519
|
+
).description || "";
|
|
520
|
+
|
|
521
|
+
expect(description).toBe(
|
|
522
|
+
"When enabled, acknowledging an incident also acknowledges every alert linked to it. This stops those alerts' on-call escalations, and their reminders only when the alert reminder rule is set to stop on Acknowledged. Alerts linked to an incident that is already acknowledged are acknowledged as they are linked.",
|
|
523
|
+
);
|
|
524
|
+
expect(description).not.toContain("escalations and reminders");
|
|
525
|
+
|
|
526
|
+
expect(
|
|
527
|
+
new AlertReminderRule().getTableColumnMetadata("stopRemindersOnState")
|
|
528
|
+
.defaultValue,
|
|
529
|
+
).toBe(ReminderStopState.Resolved);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe("the feed events a link writes", () => {
|
|
534
|
+
test.each([
|
|
535
|
+
[
|
|
536
|
+
AlertFeedEventType.LinkedToIncident,
|
|
537
|
+
"LinkedToIncident",
|
|
538
|
+
"Linked to Incident",
|
|
539
|
+
],
|
|
540
|
+
[
|
|
541
|
+
AlertFeedEventType.UnlinkedFromIncident,
|
|
542
|
+
"UnlinkedFromIncident",
|
|
543
|
+
"Unlinked from Incident",
|
|
544
|
+
],
|
|
545
|
+
[IncidentFeedEventType.AlertLinked, "AlertLinked", "Alert Linked"],
|
|
546
|
+
[IncidentFeedEventType.AlertUnlinked, "AlertUnlinked", "Alert Unlinked"],
|
|
547
|
+
])(
|
|
548
|
+
"%s is stored as %s and labelled %s",
|
|
549
|
+
(eventType: string, stored: string, label: string) => {
|
|
550
|
+
expect(eventType).toBe(stored);
|
|
551
|
+
expect(getFeedEventTypeLabel(eventType)).toBe(label);
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
});
|