@oneuptime/common 12.0.16 → 12.0.18
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/DetectionRule.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/BillingService.ts +8 -0
- package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +303 -46
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
- package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +250 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +106 -0
- package/Tests/App/Dashboard/SecurityEventsMonitorsPage.test.tsx +185 -0
- package/Tests/App/Dashboard/SecurityEventsSetupGuide.test.tsx +200 -0
- package/Tests/Models/DetectionRuleCreateContract.test.ts +35 -0
- package/Tests/Server/Services/BillingService.test.ts +20 -0
- package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +438 -0
- package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
- package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
- package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
- package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
- package/Types/Billing/PayAsYouGoPricing.ts +1 -1
- package/Types/SecurityEvent/DetectionFindingConstants.ts +24 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js +81 -0
- package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788600000000-AddDetectionRuleIncidentColumns.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/BillingService.js +6 -0
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +220 -24
- package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +18 -0
- package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import { cleanup, render } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { MemoryRouter } from "react-router-dom";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The Detection Rules page is one big ModelTable call, and everything this
|
|
16
|
+
* change added to it — the incident toggle, the two severity dropdowns
|
|
17
|
+
* with their showIf chains, the DB-default-mirroring initial values, and
|
|
18
|
+
* the "Create Monitor" row action — is configuration passed as props.
|
|
19
|
+
* Dropping any of it is type-safe and render-safe, so the props are the
|
|
20
|
+
* only place it can be pinned. The table itself is mocked to capture them;
|
|
21
|
+
* the showIf and onClick functions are then exercised directly.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type CapturedFormField = {
|
|
25
|
+
field: Record<string, boolean>;
|
|
26
|
+
title: string;
|
|
27
|
+
showIf?: ((model: Record<string, unknown>) => boolean) | undefined;
|
|
28
|
+
dropdownModal?: { type: unknown } | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CapturedActionButton = {
|
|
32
|
+
title: string;
|
|
33
|
+
disabled?: boolean | undefined;
|
|
34
|
+
tooltip?: string | undefined;
|
|
35
|
+
onClick: (
|
|
36
|
+
item: Record<string, unknown>,
|
|
37
|
+
onCompleteAction: () => void,
|
|
38
|
+
onError: (error: Error) => void,
|
|
39
|
+
) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type CapturedTableProps = {
|
|
43
|
+
formFields?: Array<CapturedFormField>;
|
|
44
|
+
actionButtons?: Array<CapturedActionButton>;
|
|
45
|
+
createInitialValues?: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let capturedTableProps: CapturedTableProps | null = null;
|
|
49
|
+
|
|
50
|
+
jest.mock("../../../UI/Components/ModelTable/ModelTable", () => {
|
|
51
|
+
return {
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: (props: CapturedTableProps) => {
|
|
54
|
+
capturedTableProps = props;
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
import DetectionRulesPage from "../../../../App/FeatureSet/Dashboard/src/Pages/SecurityEvents/DetectionRules";
|
|
61
|
+
import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
|
|
62
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
63
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
64
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
65
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
66
|
+
import PermissionGate, {
|
|
67
|
+
PermissionGateResult,
|
|
68
|
+
} from "../../../UI/Utils/PermissionGate";
|
|
69
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
70
|
+
import Route from "../../../Types/API/Route";
|
|
71
|
+
|
|
72
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
73
|
+
"11111111-1111-4111-8111-111111111111",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
function fieldTitled(title: string): CapturedFormField {
|
|
77
|
+
const field: CapturedFormField | undefined =
|
|
78
|
+
capturedTableProps?.formFields?.find(
|
|
79
|
+
(formField: CapturedFormField): boolean => {
|
|
80
|
+
return formField.title === title;
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(field).toBeDefined();
|
|
85
|
+
|
|
86
|
+
return field!;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderPage(): void {
|
|
90
|
+
const project: Project = new Project();
|
|
91
|
+
project.id = PROJECT_ID;
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<MemoryRouter>
|
|
95
|
+
<DetectionRulesPage
|
|
96
|
+
pageRoute={new Route("/dashboard/security-events/detection-rules")}
|
|
97
|
+
currentProject={project}
|
|
98
|
+
hasPaymentMethod={true}
|
|
99
|
+
/>
|
|
100
|
+
</MemoryRouter>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(capturedTableProps).not.toBeNull();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function gateMonitorCreate(result: PermissionGateResult): void {
|
|
107
|
+
jest.spyOn(PermissionGate, "check").mockReturnValue(result);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("Detection Rules page", () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
capturedTableProps = null;
|
|
113
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
114
|
+
gateMonitorCreate({ isAllowed: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
cleanup();
|
|
119
|
+
jest.restoreAllMocks();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("incident opt-in fields", () => {
|
|
123
|
+
test("the incident toggle exists on the evaluation step", () => {
|
|
124
|
+
renderPage();
|
|
125
|
+
fieldTitled("Create Incident on Match");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("the incident severity dropdown shows only when the toggle is on", () => {
|
|
129
|
+
renderPage();
|
|
130
|
+
const severityField: CapturedFormField = fieldTitled("Incident Severity");
|
|
131
|
+
|
|
132
|
+
expect(severityField.showIf).toBeDefined();
|
|
133
|
+
expect(severityField.showIf!({ shouldCreateIncident: true })).toBe(true);
|
|
134
|
+
expect(severityField.showIf!({ shouldCreateIncident: false })).toBe(
|
|
135
|
+
false,
|
|
136
|
+
);
|
|
137
|
+
// undefined must read as off — the column defaults to false.
|
|
138
|
+
expect(severityField.showIf!({})).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the incident severity dropdown selects from IncidentSeverity, not AlertSeverity", () => {
|
|
142
|
+
renderPage();
|
|
143
|
+
const severityField: CapturedFormField = fieldTitled("Incident Severity");
|
|
144
|
+
|
|
145
|
+
expect(severityField.dropdownModal?.type).toBe(IncidentSeverity);
|
|
146
|
+
expect(severityField.field).toEqual({ incidentSeverity: true });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("the alert severity dropdown chains on the alert toggle the same way", () => {
|
|
150
|
+
renderPage();
|
|
151
|
+
const severityField: CapturedFormField = fieldTitled("Alert Severity");
|
|
152
|
+
|
|
153
|
+
expect(severityField.dropdownModal?.type).toBe(AlertSeverity);
|
|
154
|
+
expect(severityField.showIf!({ shouldCreateAlert: true })).toBe(true);
|
|
155
|
+
expect(severityField.showIf!({ shouldCreateAlert: false })).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("initial values mirror the DB defaults, so the chained fields are honest on a fresh form", () => {
|
|
159
|
+
/*
|
|
160
|
+
* shouldCreateAlert defaults TRUE in the schema and
|
|
161
|
+
* shouldCreateIncident FALSE. Without these initial values a fresh
|
|
162
|
+
* create form shows the alert toggle apparently off (undefined)
|
|
163
|
+
* with its severity dropdown hidden — while saving would create an
|
|
164
|
+
* alerting rule.
|
|
165
|
+
*/
|
|
166
|
+
renderPage();
|
|
167
|
+
expect(capturedTableProps?.createInitialValues).toMatchObject({
|
|
168
|
+
shouldCreateAlert: true,
|
|
169
|
+
shouldWriteDetectionFinding: true,
|
|
170
|
+
shouldCreateIncident: false,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("Create Monitor row action", () => {
|
|
176
|
+
test("navigates to monitor create carrying the rule id", () => {
|
|
177
|
+
renderPage();
|
|
178
|
+
|
|
179
|
+
const navigateSpy: ReturnType<typeof jest.spyOn> = jest
|
|
180
|
+
.spyOn(Navigation, "navigate")
|
|
181
|
+
.mockImplementation(() => {
|
|
182
|
+
return undefined;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const button: CapturedActionButton | undefined =
|
|
186
|
+
capturedTableProps?.actionButtons?.find(
|
|
187
|
+
(actionButton: CapturedActionButton): boolean => {
|
|
188
|
+
return actionButton.title === "Create Monitor";
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(button).toBeDefined();
|
|
193
|
+
|
|
194
|
+
let completed: boolean = false;
|
|
195
|
+
|
|
196
|
+
button!.onClick(
|
|
197
|
+
{ _id: "22222222-2222-4222-8222-222222222222" },
|
|
198
|
+
() => {
|
|
199
|
+
completed = true;
|
|
200
|
+
},
|
|
201
|
+
() => {
|
|
202
|
+
// no-op
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
|
207
|
+
|
|
208
|
+
const destination: string = String(navigateSpy.mock.calls[0]?.[0]);
|
|
209
|
+
|
|
210
|
+
expect(destination).toContain("/monitors/create");
|
|
211
|
+
expect(destination).toContain(
|
|
212
|
+
"detectionRuleId=22222222-2222-4222-8222-222222222222",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// A row button that never completes spins forever.
|
|
216
|
+
expect(completed).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("is enabled with the explainer tooltip when monitor create is allowed", () => {
|
|
220
|
+
renderPage();
|
|
221
|
+
|
|
222
|
+
const button: CapturedActionButton | undefined =
|
|
223
|
+
capturedTableProps?.actionButtons?.[0];
|
|
224
|
+
|
|
225
|
+
expect(button?.disabled).toBe(false);
|
|
226
|
+
expect(button?.tooltip).toContain("Detection Findings");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("is disabled with the gate's reason when monitor create is not allowed", () => {
|
|
230
|
+
/*
|
|
231
|
+
* Same contract as MonitorTable's create button (issue #3306): a
|
|
232
|
+
* member who cannot create monitors must not be walked into the
|
|
233
|
+
* wizard to be refused at submit — the button stays visible,
|
|
234
|
+
* disabled, and says which permission is missing.
|
|
235
|
+
*/
|
|
236
|
+
gateMonitorCreate({
|
|
237
|
+
isAllowed: false,
|
|
238
|
+
disabledReason: "You need the Create Monitor permission.",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
renderPage();
|
|
242
|
+
|
|
243
|
+
const button: CapturedActionButton | undefined =
|
|
244
|
+
capturedTableProps?.actionButtons?.[0];
|
|
245
|
+
|
|
246
|
+
expect(button?.disabled).toBe(true);
|
|
247
|
+
expect(button?.tooltip).toBe("You need the Create Monitor permission.");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import SecurityEventsMonitorStepForm from "../../../../App/FeatureSet/Dashboard/src/Components/Form/Monitor/SecurityEventsMonitor/SecurityEventsMonitorStepForm";
|
|
13
|
+
import MonitorStepSecurityEventsMonitor, {
|
|
14
|
+
MonitorStepSecurityEventsMonitorUtil,
|
|
15
|
+
} from "../../../Types/Monitor/MonitorStepSecurityEventsMonitor";
|
|
16
|
+
import AnalyticsModelAPI from "../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
|
17
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
18
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The step form's field gating IS the feature here: Event Class was moved
|
|
22
|
+
* out from behind the "Show Advanced Options" toggle so that watching
|
|
23
|
+
* Detection Findings — the single most useful class filter — no longer
|
|
24
|
+
* requires knowing the toggle exists. No other test renders this form
|
|
25
|
+
* (the view-model and type tests cover different layers), so without
|
|
26
|
+
* this one a refactor could quietly put the field back behind the toggle
|
|
27
|
+
* and nothing would fail.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
31
|
+
"11111111-1111-4111-8111-111111111111",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
function renderForm(config?: Partial<MonitorStepSecurityEventsMonitor>): void {
|
|
35
|
+
render(
|
|
36
|
+
<SecurityEventsMonitorStepForm
|
|
37
|
+
monitorStepSecurityEventsMonitor={{
|
|
38
|
+
...MonitorStepSecurityEventsMonitorUtil.getDefault(),
|
|
39
|
+
...(config || {}),
|
|
40
|
+
}}
|
|
41
|
+
onMonitorStepSecurityEventsMonitorChanged={() => {
|
|
42
|
+
// no-op
|
|
43
|
+
}}
|
|
44
|
+
telemetryServices={[]}
|
|
45
|
+
/>,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("SecurityEventsMonitorStepForm field gating", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
52
|
+
// The live preview polls the event count; keep it quiet and offline.
|
|
53
|
+
jest.spyOn(AnalyticsModelAPI, "count").mockResolvedValue(0 as never);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
cleanup();
|
|
58
|
+
jest.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("Event Class is visible without opening advanced options", async () => {
|
|
62
|
+
renderForm();
|
|
63
|
+
|
|
64
|
+
// findBy: lets the preview's initial async count resolve inside act.
|
|
65
|
+
expect(await screen.findByText("Event Class")).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText("Show Advanced Options")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("severity, service and attribute filters stay behind the toggle", async () => {
|
|
70
|
+
renderForm();
|
|
71
|
+
|
|
72
|
+
await screen.findByText("Event Class");
|
|
73
|
+
|
|
74
|
+
expect(screen.queryByText("Event Severity")).not.toBeInTheDocument();
|
|
75
|
+
expect(
|
|
76
|
+
screen.queryByText("Filter by Telemetry Service"),
|
|
77
|
+
).not.toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByText("Filter by Attributes")).not.toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
fireEvent.click(screen.getByText("Show Advanced Options"));
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText("Event Severity")).toBeInTheDocument();
|
|
83
|
+
expect(screen.getByText("Filter by Telemetry Service")).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText("Filter by Attributes")).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a stored severity filter still opens the advanced section by itself", async () => {
|
|
88
|
+
/*
|
|
89
|
+
* Editing a monitor whose config uses an advanced filter must show
|
|
90
|
+
* that filter immediately — the auto-open behaviour classNames used
|
|
91
|
+
* to share before it stopped being advanced.
|
|
92
|
+
*/
|
|
93
|
+
renderForm({ severityNames: ["High" as never] });
|
|
94
|
+
|
|
95
|
+
expect(await screen.findByText("Event Severity")).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByText("Hide Advanced Options")).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("a stored class filter alone no longer forces the advanced section open", async () => {
|
|
100
|
+
renderForm({ classNames: ["Detection Finding"] });
|
|
101
|
+
|
|
102
|
+
expect(await screen.findByText("Event Class")).toBeInTheDocument();
|
|
103
|
+
expect(screen.queryByText("Event Severity")).not.toBeInTheDocument();
|
|
104
|
+
expect(screen.getByText("Show Advanced Options")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { MemoryRouter } from "react-router-dom";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The Security Events → Monitors tab is a thin composition over
|
|
16
|
+
* MonitorTable, and the two things that make it correct are both props:
|
|
17
|
+
* the base query that scopes the table to MonitorType.SecurityEvents, and
|
|
18
|
+
* the replacement create button that deep-links into the gated monitor
|
|
19
|
+
* create page with the type preselected. Neither failing looks wrong on
|
|
20
|
+
* screen — an unscoped table just shows more rows, and a plain create
|
|
21
|
+
* button just asks the user to pick the type again — so the captured
|
|
22
|
+
* props are what gets pinned.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type CapturedCardButton = {
|
|
26
|
+
title: string;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CapturedMonitorTableProps = {
|
|
31
|
+
query?: Record<string, unknown>;
|
|
32
|
+
disableCreate?: boolean;
|
|
33
|
+
cardButtons?: Array<CapturedCardButton>;
|
|
34
|
+
saveFilterProps?: { tableId?: string };
|
|
35
|
+
title?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let capturedMonitorTableProps: CapturedMonitorTableProps | null = null;
|
|
39
|
+
|
|
40
|
+
jest.mock(
|
|
41
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Monitor/MonitorTable",
|
|
42
|
+
() => {
|
|
43
|
+
return {
|
|
44
|
+
__esModule: true,
|
|
45
|
+
default: (props: CapturedMonitorTableProps) => {
|
|
46
|
+
capturedMonitorTableProps = props;
|
|
47
|
+
return null;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
import SecurityEventsMonitorsPage from "../../../../App/FeatureSet/Dashboard/src/Pages/SecurityEvents/Monitors";
|
|
54
|
+
import MonitorType from "../../../Types/Monitor/MonitorType";
|
|
55
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
56
|
+
import Reseller from "../../../Models/DatabaseModels/Reseller";
|
|
57
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
58
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
59
|
+
import PermissionGate from "../../../UI/Utils/PermissionGate";
|
|
60
|
+
import { CardButtonSchema } from "../../../UI/Components/Card/Card";
|
|
61
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
62
|
+
import Route from "../../../Types/API/Route";
|
|
63
|
+
|
|
64
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
65
|
+
"11111111-1111-4111-8111-111111111111",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
function renderPage(project: Project): void {
|
|
69
|
+
render(
|
|
70
|
+
<MemoryRouter>
|
|
71
|
+
<SecurityEventsMonitorsPage
|
|
72
|
+
pageRoute={new Route("/dashboard/security-events/monitors")}
|
|
73
|
+
currentProject={project}
|
|
74
|
+
hasPaymentMethod={true}
|
|
75
|
+
/>
|
|
76
|
+
</MemoryRouter>,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildProject(options: { telemetryDisabled?: boolean } = {}): Project {
|
|
81
|
+
const project: Project = new Project();
|
|
82
|
+
project.id = PROJECT_ID;
|
|
83
|
+
|
|
84
|
+
if (options.telemetryDisabled) {
|
|
85
|
+
const reseller: Reseller = new Reseller();
|
|
86
|
+
reseller.enableTelemetryFeatures = false;
|
|
87
|
+
project.reseller = reseller;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return project;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("Security Events monitors page", () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
capturedMonitorTableProps = null;
|
|
96
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
97
|
+
/*
|
|
98
|
+
* Pass the button through unchanged: what the PERMISSION gate does is
|
|
99
|
+
* PermissionGate's own test's problem; this page's contract is that
|
|
100
|
+
* the button it hands the gate deep-links correctly.
|
|
101
|
+
*/
|
|
102
|
+
jest
|
|
103
|
+
.spyOn(PermissionGate, "gateCardButton")
|
|
104
|
+
.mockImplementation((button: CardButtonSchema) => {
|
|
105
|
+
return button;
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
cleanup();
|
|
111
|
+
jest.restoreAllMocks();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("scopes the table to Security Events monitors in this project", () => {
|
|
115
|
+
renderPage(buildProject());
|
|
116
|
+
|
|
117
|
+
expect(capturedMonitorTableProps).not.toBeNull();
|
|
118
|
+
expect(capturedMonitorTableProps?.query).toMatchObject({
|
|
119
|
+
monitorType: MonitorType.SecurityEvents,
|
|
120
|
+
});
|
|
121
|
+
expect(capturedMonitorTableProps?.query?.["projectId"]?.toString()).toBe(
|
|
122
|
+
PROJECT_ID.toString(),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("keeps its filter state under its own table id", () => {
|
|
127
|
+
renderPage(buildProject());
|
|
128
|
+
|
|
129
|
+
expect(capturedMonitorTableProps?.saveFilterProps?.tableId).toBe(
|
|
130
|
+
"security-events-monitors-table",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("replaces the built-in create with a type-preselecting deep link", () => {
|
|
135
|
+
renderPage(buildProject());
|
|
136
|
+
|
|
137
|
+
expect(capturedMonitorTableProps?.disableCreate).toBe(true);
|
|
138
|
+
|
|
139
|
+
const button: CapturedCardButton | undefined =
|
|
140
|
+
capturedMonitorTableProps?.cardButtons?.[0];
|
|
141
|
+
|
|
142
|
+
expect(button?.title).toBe("Create Security Events Monitor");
|
|
143
|
+
|
|
144
|
+
const navigateSpy: ReturnType<typeof jest.spyOn> = jest
|
|
145
|
+
.spyOn(Navigation, "navigate")
|
|
146
|
+
.mockImplementation(() => {
|
|
147
|
+
return undefined;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
button!.onClick();
|
|
151
|
+
|
|
152
|
+
const destination: string = String(navigateSpy.mock.calls[0]?.[0]);
|
|
153
|
+
|
|
154
|
+
expect(destination).toContain("/monitors/create");
|
|
155
|
+
/*
|
|
156
|
+
* Encoded, not raw: the enum value carries a space, which Route's
|
|
157
|
+
* character validator rejects — this exact assertion is what caught
|
|
158
|
+
* the crash the first time.
|
|
159
|
+
*/
|
|
160
|
+
expect(destination).toContain(
|
|
161
|
+
`monitorType=${encodeURIComponent(MonitorType.SecurityEvents)}`,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("drops the create button entirely when the permission gate says no", () => {
|
|
166
|
+
jest
|
|
167
|
+
.spyOn(PermissionGate, "gateCardButton")
|
|
168
|
+
.mockImplementation((): CardButtonSchema | null => {
|
|
169
|
+
return null;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
renderPage(buildProject());
|
|
173
|
+
|
|
174
|
+
expect(capturedMonitorTableProps?.cardButtons).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("shows the reseller telemetry gate instead of the table", () => {
|
|
178
|
+
renderPage(buildProject({ telemetryDisabled: true }));
|
|
179
|
+
|
|
180
|
+
expect(capturedMonitorTableProps).toBeNull();
|
|
181
|
+
expect(
|
|
182
|
+
screen.getByText(/did not include telemetry features/i),
|
|
183
|
+
).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
});
|