@oneuptime/common 12.0.10 → 12.0.12
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
- package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Server/Utils/VM/VMRunner.ts +693 -874
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.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/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { beforeEach, describe, expect, it } from "@jest/globals";
|
|
6
|
+
import { PlanType } from "../../../Types/Billing/SubscriptionPlan";
|
|
7
|
+
import MonitorType from "../../../Types/Monitor/MonitorType";
|
|
8
|
+
import Permission, { UserPermission } from "../../../Types/Permission";
|
|
9
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
10
|
+
import TelemetryIngestionKey from "../../../Models/DatabaseModels/TelemetryIngestionKey";
|
|
11
|
+
import FormFieldSchemaType from "../../../UI/Components/Forms/Types/FormFieldSchemaType";
|
|
12
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
13
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
14
|
+
import { getJestSpyOn } from "../../Spy";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The consent checkbox is the whole point of the change: on the Free plan a
|
|
18
|
+
* user must not be able to start a metered charge without saying they
|
|
19
|
+
* understand it. This suite drives the real ModelForm rather than the field
|
|
20
|
+
* definitions, because the mechanism is subtle - the form's own `required`
|
|
21
|
+
* check stringifies values, so an unticked box reads as the non-empty string
|
|
22
|
+
* "false" and sails straight through it. The gate is a customValidation, and
|
|
23
|
+
* it only runs at all because getDefaultValue seeds the key.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const createOrUpdateMock: MockFunction = getJestMockFunction();
|
|
27
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
28
|
+
|
|
29
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
30
|
+
return {
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: {
|
|
33
|
+
getItem: (...args: Array<any>) => {
|
|
34
|
+
return getItemMock(...args);
|
|
35
|
+
},
|
|
36
|
+
createOrUpdate: (...args: Array<any>) => {
|
|
37
|
+
return createOrUpdateMock(...args);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const OWNER_PERMISSIONS: Array<Permission> = [
|
|
44
|
+
Permission.Public,
|
|
45
|
+
Permission.User,
|
|
46
|
+
Permission.CurrentUser,
|
|
47
|
+
Permission.ProjectOwner,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
jest.mock("../../../UI/Utils/Permission", () => {
|
|
51
|
+
return {
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: {
|
|
54
|
+
getAllPermissions: () => {
|
|
55
|
+
return OWNER_PERMISSIONS;
|
|
56
|
+
},
|
|
57
|
+
getProjectPermissions: () => {
|
|
58
|
+
return {
|
|
59
|
+
permissions: OWNER_PERMISSIONS.map((permission: Permission) => {
|
|
60
|
+
return {
|
|
61
|
+
permission,
|
|
62
|
+
labelIds: [],
|
|
63
|
+
_type: "UserPermission",
|
|
64
|
+
} as UserPermission;
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
getGlobalPermissions: () => {
|
|
69
|
+
return { globalPermissions: OWNER_PERMISSIONS };
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
jest.mock("../../../UI/Utils/User", () => {
|
|
76
|
+
return {
|
|
77
|
+
__esModule: true,
|
|
78
|
+
default: {
|
|
79
|
+
isMasterAdmin: () => {
|
|
80
|
+
return false;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
interface MutableConfig {
|
|
87
|
+
billingEnabled: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const config: MutableConfig = { billingEnabled: true };
|
|
91
|
+
|
|
92
|
+
(
|
|
93
|
+
globalThis as unknown as { __payAsYouGoGateConfig: MutableConfig }
|
|
94
|
+
).__payAsYouGoGateConfig = config;
|
|
95
|
+
|
|
96
|
+
jest.mock("../../../UI/Config", () => {
|
|
97
|
+
const mocked: Record<string, unknown> = {
|
|
98
|
+
...jest.requireActual("../../../UI/Config"),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
Object.defineProperty(mocked, "BILLING_ENABLED", {
|
|
102
|
+
get: (): boolean => {
|
|
103
|
+
return Boolean(
|
|
104
|
+
(
|
|
105
|
+
globalThis as unknown as {
|
|
106
|
+
__payAsYouGoGateConfig: MutableConfig | undefined;
|
|
107
|
+
}
|
|
108
|
+
).__payAsYouGoGateConfig?.billingEnabled,
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return mocked;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Imported after the mocks above.
|
|
117
|
+
import ModelForm, { FormType } from "../../../UI/Components/Forms/ModelForm";
|
|
118
|
+
import {
|
|
119
|
+
MONITOR_CONSENT_ERROR,
|
|
120
|
+
MONITOR_CONSENT_FIELD_KEY,
|
|
121
|
+
TELEMETRY_CONSENT_ERROR,
|
|
122
|
+
TELEMETRY_CONSENT_FIELD_KEY,
|
|
123
|
+
getMonitorPayAsYouGoFormFields,
|
|
124
|
+
getTelemetryPayAsYouGoFormFields,
|
|
125
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Billing/PayAsYouGo";
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* These render real forms that validate on every keystroke, so give the waits
|
|
129
|
+
* room to survive a loaded CI box.
|
|
130
|
+
*/
|
|
131
|
+
const WAIT_TIMEOUT: number = 20000;
|
|
132
|
+
|
|
133
|
+
type RenderIngestionKeyFormFunction = () => void;
|
|
134
|
+
|
|
135
|
+
const renderIngestionKeyForm: RenderIngestionKeyFormFunction = (): void => {
|
|
136
|
+
render(
|
|
137
|
+
<ModelForm<TelemetryIngestionKey>
|
|
138
|
+
modelType={TelemetryIngestionKey}
|
|
139
|
+
id="create-ingestion-key-form"
|
|
140
|
+
name="Create Ingestion Key"
|
|
141
|
+
formType={FormType.Create}
|
|
142
|
+
submitButtonText="Create Ingestion Key"
|
|
143
|
+
fields={[
|
|
144
|
+
...getTelemetryPayAsYouGoFormFields(),
|
|
145
|
+
{
|
|
146
|
+
field: { name: true },
|
|
147
|
+
title: "Name",
|
|
148
|
+
fieldType: FormFieldSchemaType.Text,
|
|
149
|
+
required: true,
|
|
150
|
+
dataTestId: "ingestion-key-name",
|
|
151
|
+
},
|
|
152
|
+
]}
|
|
153
|
+
/>,
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type RenderMonitorFormFunction = (monitorType: MonitorType) => void;
|
|
158
|
+
|
|
159
|
+
const renderMonitorForm: RenderMonitorFormFunction = (
|
|
160
|
+
monitorType: MonitorType,
|
|
161
|
+
): void => {
|
|
162
|
+
render(
|
|
163
|
+
<ModelForm<Monitor>
|
|
164
|
+
modelType={Monitor}
|
|
165
|
+
id="create-monitor-form"
|
|
166
|
+
name="Create New Monitor"
|
|
167
|
+
formType={FormType.Create}
|
|
168
|
+
submitButtonText="Create Monitor"
|
|
169
|
+
initialValues={{ monitorType: monitorType } as any}
|
|
170
|
+
steps={[{ title: "Monitor Info", id: "monitor-info" }]}
|
|
171
|
+
fields={[
|
|
172
|
+
{
|
|
173
|
+
field: { name: true },
|
|
174
|
+
stepId: "monitor-info",
|
|
175
|
+
title: "Name",
|
|
176
|
+
fieldType: FormFieldSchemaType.Text,
|
|
177
|
+
required: true,
|
|
178
|
+
dataTestId: "monitor-name",
|
|
179
|
+
},
|
|
180
|
+
...getMonitorPayAsYouGoFormFields({ stepId: "monitor-info" }),
|
|
181
|
+
]}
|
|
182
|
+
/>,
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
describe("Pay as you go consent gate", () => {
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
jest.restoreAllMocks();
|
|
189
|
+
config.billingEnabled = true;
|
|
190
|
+
createOrUpdateMock.mockReset();
|
|
191
|
+
getItemMock.mockReset();
|
|
192
|
+
createOrUpdateMock.mockResolvedValue({ data: {} });
|
|
193
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(PlanType.Free);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("creating a telemetry ingestion key on the Free plan", () => {
|
|
197
|
+
it("shows the pay as you go notice and the consent box in the form", async () => {
|
|
198
|
+
renderIngestionKeyForm();
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
await screen.findByText(
|
|
202
|
+
"Telemetry is a pay as you go feature",
|
|
203
|
+
{},
|
|
204
|
+
{ timeout: WAIT_TIMEOUT },
|
|
205
|
+
),
|
|
206
|
+
).toBeInTheDocument();
|
|
207
|
+
expect(
|
|
208
|
+
screen.getByTestId("telemetry-pay-as-you-go-consent"),
|
|
209
|
+
).toBeInTheDocument();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("refuses to create the key until the charge is acknowledged", async () => {
|
|
213
|
+
renderIngestionKeyForm();
|
|
214
|
+
|
|
215
|
+
await userEvent.type(
|
|
216
|
+
await screen.findByTestId(
|
|
217
|
+
"ingestion-key-name",
|
|
218
|
+
{},
|
|
219
|
+
{ timeout: WAIT_TIMEOUT },
|
|
220
|
+
),
|
|
221
|
+
"Production",
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await userEvent.click(screen.getByText("Create Ingestion Key"));
|
|
225
|
+
|
|
226
|
+
await waitFor(
|
|
227
|
+
() => {
|
|
228
|
+
expect(screen.getByText(TELEMETRY_CONSENT_ERROR)).toBeInTheDocument();
|
|
229
|
+
},
|
|
230
|
+
{ timeout: WAIT_TIMEOUT },
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(createOrUpdateMock).not.toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("creates the key once the box is ticked", async () => {
|
|
237
|
+
renderIngestionKeyForm();
|
|
238
|
+
|
|
239
|
+
await userEvent.type(
|
|
240
|
+
await screen.findByTestId(
|
|
241
|
+
"ingestion-key-name",
|
|
242
|
+
{},
|
|
243
|
+
{ timeout: WAIT_TIMEOUT },
|
|
244
|
+
),
|
|
245
|
+
"Production",
|
|
246
|
+
);
|
|
247
|
+
await userEvent.click(
|
|
248
|
+
screen.getByTestId("telemetry-pay-as-you-go-consent"),
|
|
249
|
+
);
|
|
250
|
+
await userEvent.click(screen.getByText("Create Ingestion Key"));
|
|
251
|
+
|
|
252
|
+
await waitFor(
|
|
253
|
+
() => {
|
|
254
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
255
|
+
},
|
|
256
|
+
{ timeout: WAIT_TIMEOUT },
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("blocks again if the box is ticked and then unticked", async () => {
|
|
261
|
+
/*
|
|
262
|
+
* The regression this guards: an unticked box that has been touched
|
|
263
|
+
* holds boolean false, which the form's `required` check would accept.
|
|
264
|
+
*/
|
|
265
|
+
renderIngestionKeyForm();
|
|
266
|
+
|
|
267
|
+
await userEvent.type(
|
|
268
|
+
await screen.findByTestId(
|
|
269
|
+
"ingestion-key-name",
|
|
270
|
+
{},
|
|
271
|
+
{ timeout: WAIT_TIMEOUT },
|
|
272
|
+
),
|
|
273
|
+
"Production",
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const consent: HTMLElement = screen.getByTestId(
|
|
277
|
+
"telemetry-pay-as-you-go-consent",
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await userEvent.click(consent);
|
|
281
|
+
await userEvent.click(consent);
|
|
282
|
+
await userEvent.click(screen.getByText("Create Ingestion Key"));
|
|
283
|
+
|
|
284
|
+
await waitFor(
|
|
285
|
+
() => {
|
|
286
|
+
expect(screen.getByText(TELEMETRY_CONSENT_ERROR)).toBeInTheDocument();
|
|
287
|
+
},
|
|
288
|
+
{ timeout: WAIT_TIMEOUT },
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(createOrUpdateMock).not.toHaveBeenCalled();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("does not send the acknowledgement to the API", async () => {
|
|
295
|
+
renderIngestionKeyForm();
|
|
296
|
+
|
|
297
|
+
await userEvent.type(
|
|
298
|
+
await screen.findByTestId(
|
|
299
|
+
"ingestion-key-name",
|
|
300
|
+
{},
|
|
301
|
+
{ timeout: WAIT_TIMEOUT },
|
|
302
|
+
),
|
|
303
|
+
"Production",
|
|
304
|
+
);
|
|
305
|
+
await userEvent.click(
|
|
306
|
+
screen.getByTestId("telemetry-pay-as-you-go-consent"),
|
|
307
|
+
);
|
|
308
|
+
await userEvent.click(screen.getByText("Create Ingestion Key"));
|
|
309
|
+
|
|
310
|
+
await waitFor(
|
|
311
|
+
() => {
|
|
312
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
313
|
+
},
|
|
314
|
+
{ timeout: WAIT_TIMEOUT },
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const call: any = createOrUpdateMock.mock.calls[0]?.[0];
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* The consent is a UI gate, not data. It has no column, so sending it
|
|
321
|
+
* would be an unknown property on the create payload.
|
|
322
|
+
*/
|
|
323
|
+
expect(call?.miscDataProps ?? {}).not.toHaveProperty(
|
|
324
|
+
TELEMETRY_CONSENT_FIELD_KEY,
|
|
325
|
+
);
|
|
326
|
+
expect(JSON.stringify(call?.model ?? {})).not.toContain(
|
|
327
|
+
TELEMETRY_CONSENT_FIELD_KEY,
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("creating a telemetry ingestion key off the Free plan", () => {
|
|
333
|
+
it("asks for nothing extra on a paid plan", async () => {
|
|
334
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(
|
|
335
|
+
PlanType.Growth,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
renderIngestionKeyForm();
|
|
339
|
+
|
|
340
|
+
await userEvent.type(
|
|
341
|
+
await screen.findByTestId(
|
|
342
|
+
"ingestion-key-name",
|
|
343
|
+
{},
|
|
344
|
+
{ timeout: WAIT_TIMEOUT },
|
|
345
|
+
),
|
|
346
|
+
"Production",
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(
|
|
350
|
+
screen.queryByTestId("telemetry-pay-as-you-go-consent"),
|
|
351
|
+
).not.toBeInTheDocument();
|
|
352
|
+
expect(
|
|
353
|
+
screen.queryByText("Telemetry is a pay as you go feature"),
|
|
354
|
+
).not.toBeInTheDocument();
|
|
355
|
+
|
|
356
|
+
await userEvent.click(screen.getByText("Create Ingestion Key"));
|
|
357
|
+
|
|
358
|
+
await waitFor(
|
|
359
|
+
() => {
|
|
360
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
361
|
+
},
|
|
362
|
+
{ timeout: WAIT_TIMEOUT },
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("asks for nothing extra on a self-hosted install", async () => {
|
|
367
|
+
config.billingEnabled = false;
|
|
368
|
+
|
|
369
|
+
renderIngestionKeyForm();
|
|
370
|
+
|
|
371
|
+
await userEvent.type(
|
|
372
|
+
await screen.findByTestId(
|
|
373
|
+
"ingestion-key-name",
|
|
374
|
+
{},
|
|
375
|
+
{ timeout: WAIT_TIMEOUT },
|
|
376
|
+
),
|
|
377
|
+
"Production",
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(
|
|
381
|
+
screen.queryByTestId("telemetry-pay-as-you-go-consent"),
|
|
382
|
+
).not.toBeInTheDocument();
|
|
383
|
+
|
|
384
|
+
await userEvent.click(screen.getByText("Create Ingestion Key"));
|
|
385
|
+
|
|
386
|
+
await waitFor(
|
|
387
|
+
() => {
|
|
388
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
389
|
+
},
|
|
390
|
+
{ timeout: WAIT_TIMEOUT },
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("creating a monitor on the Free plan", () => {
|
|
396
|
+
it("refuses to create a billed monitor until the charge is acknowledged", async () => {
|
|
397
|
+
renderMonitorForm(MonitorType.Website);
|
|
398
|
+
|
|
399
|
+
await userEvent.type(
|
|
400
|
+
await screen.findByTestId(
|
|
401
|
+
"monitor-name",
|
|
402
|
+
{},
|
|
403
|
+
{ timeout: WAIT_TIMEOUT },
|
|
404
|
+
),
|
|
405
|
+
"Marketing site",
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(
|
|
409
|
+
screen.getByTestId("monitor-pay-as-you-go-consent"),
|
|
410
|
+
).toBeInTheDocument();
|
|
411
|
+
|
|
412
|
+
await userEvent.click(screen.getByText("Create Monitor"));
|
|
413
|
+
|
|
414
|
+
await waitFor(
|
|
415
|
+
() => {
|
|
416
|
+
expect(screen.getByText(MONITOR_CONSENT_ERROR)).toBeInTheDocument();
|
|
417
|
+
},
|
|
418
|
+
{ timeout: WAIT_TIMEOUT },
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
expect(createOrUpdateMock).not.toHaveBeenCalled();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("creates the billed monitor once the box is ticked", async () => {
|
|
425
|
+
renderMonitorForm(MonitorType.Website);
|
|
426
|
+
|
|
427
|
+
await userEvent.type(
|
|
428
|
+
await screen.findByTestId(
|
|
429
|
+
"monitor-name",
|
|
430
|
+
{},
|
|
431
|
+
{ timeout: WAIT_TIMEOUT },
|
|
432
|
+
),
|
|
433
|
+
"Marketing site",
|
|
434
|
+
);
|
|
435
|
+
await userEvent.click(
|
|
436
|
+
screen.getByTestId("monitor-pay-as-you-go-consent"),
|
|
437
|
+
);
|
|
438
|
+
await userEvent.click(screen.getByText("Create Monitor"));
|
|
439
|
+
|
|
440
|
+
await waitFor(
|
|
441
|
+
() => {
|
|
442
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
443
|
+
},
|
|
444
|
+
{ timeout: WAIT_TIMEOUT },
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const call: any = createOrUpdateMock.mock.calls[0]?.[0];
|
|
448
|
+
|
|
449
|
+
expect(call?.miscDataProps ?? {}).not.toHaveProperty(
|
|
450
|
+
MONITOR_CONSENT_FIELD_KEY,
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("asks nothing of a Manual monitor - those are free", async () => {
|
|
455
|
+
renderMonitorForm(MonitorType.Manual);
|
|
456
|
+
|
|
457
|
+
await userEvent.type(
|
|
458
|
+
await screen.findByTestId(
|
|
459
|
+
"monitor-name",
|
|
460
|
+
{},
|
|
461
|
+
{ timeout: WAIT_TIMEOUT },
|
|
462
|
+
),
|
|
463
|
+
"Runbook step",
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(
|
|
467
|
+
screen.queryByTestId("monitor-pay-as-you-go-consent"),
|
|
468
|
+
).not.toBeInTheDocument();
|
|
469
|
+
|
|
470
|
+
await userEvent.click(screen.getByText("Create Monitor"));
|
|
471
|
+
|
|
472
|
+
await waitFor(
|
|
473
|
+
() => {
|
|
474
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
475
|
+
},
|
|
476
|
+
{ timeout: WAIT_TIMEOUT },
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it.each([
|
|
481
|
+
MonitorType.API,
|
|
482
|
+
MonitorType.Logs,
|
|
483
|
+
MonitorType.IncomingRequest,
|
|
484
|
+
MonitorType.SSLCertificate,
|
|
485
|
+
])(
|
|
486
|
+
"blocks %s too - every non-Manual type is billed",
|
|
487
|
+
async (monitorType: MonitorType) => {
|
|
488
|
+
renderMonitorForm(monitorType);
|
|
489
|
+
|
|
490
|
+
await userEvent.type(
|
|
491
|
+
await screen.findByTestId(
|
|
492
|
+
"monitor-name",
|
|
493
|
+
{},
|
|
494
|
+
{ timeout: WAIT_TIMEOUT },
|
|
495
|
+
),
|
|
496
|
+
"Something",
|
|
497
|
+
);
|
|
498
|
+
await userEvent.click(screen.getByText("Create Monitor"));
|
|
499
|
+
|
|
500
|
+
await waitFor(
|
|
501
|
+
() => {
|
|
502
|
+
expect(screen.getByText(MONITOR_CONSENT_ERROR)).toBeInTheDocument();
|
|
503
|
+
},
|
|
504
|
+
{ timeout: WAIT_TIMEOUT },
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
expect(createOrUpdateMock).not.toHaveBeenCalled();
|
|
508
|
+
},
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("creating a monitor off the Free plan", () => {
|
|
513
|
+
it("asks for nothing extra on a paid plan", async () => {
|
|
514
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(
|
|
515
|
+
PlanType.Growth,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
renderMonitorForm(MonitorType.Website);
|
|
519
|
+
|
|
520
|
+
await userEvent.type(
|
|
521
|
+
await screen.findByTestId(
|
|
522
|
+
"monitor-name",
|
|
523
|
+
{},
|
|
524
|
+
{ timeout: WAIT_TIMEOUT },
|
|
525
|
+
),
|
|
526
|
+
"Marketing site",
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(
|
|
530
|
+
screen.queryByTestId("monitor-pay-as-you-go-consent"),
|
|
531
|
+
).not.toBeInTheDocument();
|
|
532
|
+
|
|
533
|
+
await userEvent.click(screen.getByText("Create Monitor"));
|
|
534
|
+
|
|
535
|
+
await waitFor(
|
|
536
|
+
() => {
|
|
537
|
+
expect(createOrUpdateMock).toHaveBeenCalled();
|
|
538
|
+
},
|
|
539
|
+
{ timeout: WAIT_TIMEOUT },
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "@jest/globals";
|
|
2
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
3
|
+
import { PlanType } from "../../../Types/Billing/SubscriptionPlan";
|
|
4
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
5
|
+
import { getJestSpyOn } from "../../Spy";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* isProjectOnFreePlan decides whether a user is shown a price and made to
|
|
9
|
+
* acknowledge a charge. Two failure directions matter and they are not
|
|
10
|
+
* symmetric: quoting money to a self-hosted user who will never be invoiced is
|
|
11
|
+
* a lie, and blocking their create button over it is a bug. So the predicate
|
|
12
|
+
* fails closed, and every "we are not sure" path is pinned below.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface MutableConfig {
|
|
16
|
+
billingEnabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config: MutableConfig = { billingEnabled: true };
|
|
20
|
+
|
|
21
|
+
(
|
|
22
|
+
globalThis as unknown as { __payAsYouGoConfig: MutableConfig }
|
|
23
|
+
).__payAsYouGoConfig = config;
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* BILLING_ENABLED is a module-scope const read at import time, so it has to be
|
|
27
|
+
* switchable per test rather than per file. defineProperty rather than a
|
|
28
|
+
* getter in the object literal: a literal getter is read once while the object
|
|
29
|
+
* is built, which would freeze the value at mock time.
|
|
30
|
+
*/
|
|
31
|
+
jest.mock("../../../UI/Config", () => {
|
|
32
|
+
const mocked: Record<string, unknown> = {
|
|
33
|
+
...jest.requireActual("../../../UI/Config"),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Object.defineProperty(mocked, "BILLING_ENABLED", {
|
|
37
|
+
get: (): boolean => {
|
|
38
|
+
return Boolean(
|
|
39
|
+
(
|
|
40
|
+
globalThis as unknown as {
|
|
41
|
+
__payAsYouGoConfig: MutableConfig | undefined;
|
|
42
|
+
}
|
|
43
|
+
).__payAsYouGoConfig?.billingEnabled,
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return mocked;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Imported after the mock above so it picks the switchable BILLING_ENABLED up.
|
|
52
|
+
import { isProjectOnFreePlan } from "../../../../App/FeatureSet/Dashboard/src/Utils/PayAsYouGo";
|
|
53
|
+
|
|
54
|
+
describe("isProjectOnFreePlan", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.restoreAllMocks();
|
|
57
|
+
config.billingEnabled = true;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("on a billed (cloud) deployment", () => {
|
|
61
|
+
it("is true on the Free plan - the one plan that pays as it goes", () => {
|
|
62
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(
|
|
63
|
+
PlanType.Free,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(isProjectOnFreePlan()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.each([PlanType.Growth, PlanType.Scale, PlanType.Enterprise])(
|
|
70
|
+
"is false on the %s plan - that project already agreed to be billed",
|
|
71
|
+
(plan: PlanType) => {
|
|
72
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(plan);
|
|
73
|
+
|
|
74
|
+
expect(isProjectOnFreePlan()).toBe(false);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
it("is false when no plan is known yet - the project is not cached", () => {
|
|
79
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(null);
|
|
80
|
+
|
|
81
|
+
expect(isProjectOnFreePlan()).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is false, rather than throwing, when the plan id is not in this deployment's plan table", () => {
|
|
85
|
+
/*
|
|
86
|
+
* getCurrentPlan raises BadDataException for a plan id with no
|
|
87
|
+
* SUBSCRIPTION_PLAN_* entry. A billing notice must never be able to take
|
|
88
|
+
* a page down.
|
|
89
|
+
*/
|
|
90
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockImplementation(() => {
|
|
91
|
+
throw new BadDataException("Plan ID is invalid");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(() => {
|
|
95
|
+
return isProjectOnFreePlan();
|
|
96
|
+
}).not.toThrow();
|
|
97
|
+
expect(isProjectOnFreePlan()).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("on a self-hosted install (billing disabled)", () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
config.billingEnabled = false;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("is false even though there is no paid plan - nothing is charged", () => {
|
|
107
|
+
getJestSpyOn(ProjectUtil, "getCurrentPlan").mockReturnValue(
|
|
108
|
+
PlanType.Free,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(isProjectOnFreePlan()).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("does not even ask what the plan is", () => {
|
|
115
|
+
const getCurrentPlan: jest.SpyInstance<any, any> = getJestSpyOn(
|
|
116
|
+
ProjectUtil,
|
|
117
|
+
"getCurrentPlan",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
isProjectOnFreePlan();
|
|
121
|
+
|
|
122
|
+
expect(getCurrentPlan).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|