@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,385 @@
|
|
|
1
|
+
import VMRunner from "../../../../Server/Utils/VM/VMRunner";
|
|
2
|
+
import ReturnResult from "../../../../Types/IsolatedVM/ReturnResult";
|
|
3
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
4
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
|
|
7
|
+
jest.setTimeout(60000);
|
|
8
|
+
|
|
9
|
+
async function runInSandbox(data: {
|
|
10
|
+
code: string;
|
|
11
|
+
args?: JSONObject | undefined;
|
|
12
|
+
timeout?: number | undefined;
|
|
13
|
+
}): Promise<ReturnResult> {
|
|
14
|
+
return VMRunner.runCodeInSandbox({
|
|
15
|
+
code: data.code,
|
|
16
|
+
options: {
|
|
17
|
+
args: data.args,
|
|
18
|
+
timeout: data.timeout || 5000,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("VMRunner isolated-vm boundary", () => {
|
|
24
|
+
test("does not expose the legacy Node vm runner", () => {
|
|
25
|
+
const runnerApi: Record<string, unknown> = VMRunner as unknown as Record<
|
|
26
|
+
string,
|
|
27
|
+
unknown
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
expect(runnerApi["runCodeInNodeVM"]).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("does not expose Reference methods or temporary host globals", async () => {
|
|
34
|
+
const result: ReturnResult = await runInSandbox({
|
|
35
|
+
code: `
|
|
36
|
+
const privateNames = [
|
|
37
|
+
'_log',
|
|
38
|
+
'_captureMetric',
|
|
39
|
+
'_args',
|
|
40
|
+
'_axiosRef',
|
|
41
|
+
'_cryptoRef',
|
|
42
|
+
'_sleepRef',
|
|
43
|
+
'__oneuptimeHostLogCallback',
|
|
44
|
+
'__oneuptimeHostMetricCallback',
|
|
45
|
+
'__oneuptimeCopiedArgs',
|
|
46
|
+
'__oneuptimeHostAxiosCallback',
|
|
47
|
+
'__oneuptimeHostAxiosStartCallback',
|
|
48
|
+
'__oneuptimeHostAxiosPollCallback',
|
|
49
|
+
'__oneuptimeHostCryptoCallback',
|
|
50
|
+
'__oneuptimeHostSleepCallback',
|
|
51
|
+
'__oneuptimeHostSleepStartCallback',
|
|
52
|
+
'__oneuptimeHostSleepPollCallback',
|
|
53
|
+
];
|
|
54
|
+
const referenceMethods = [
|
|
55
|
+
'applySync',
|
|
56
|
+
'applySyncPromise',
|
|
57
|
+
'copy',
|
|
58
|
+
'copySync',
|
|
59
|
+
'deref',
|
|
60
|
+
'derefInto',
|
|
61
|
+
'getSync',
|
|
62
|
+
'release',
|
|
63
|
+
'setSync',
|
|
64
|
+
];
|
|
65
|
+
const publicValues = [
|
|
66
|
+
console,
|
|
67
|
+
console.log,
|
|
68
|
+
oneuptime,
|
|
69
|
+
oneuptime.captureMetric,
|
|
70
|
+
args,
|
|
71
|
+
axios,
|
|
72
|
+
axios.get,
|
|
73
|
+
crypto,
|
|
74
|
+
crypto.randomUUID,
|
|
75
|
+
setTimeout,
|
|
76
|
+
sleep,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
privateNamesPresent: privateNames.filter(name => name in globalThis),
|
|
81
|
+
valuesWithReferenceMethods: publicValues.map((value, index) => ({
|
|
82
|
+
index,
|
|
83
|
+
methods: referenceMethods.filter(method => {
|
|
84
|
+
try { return typeof value[method] !== 'undefined'; }
|
|
85
|
+
catch (_) { return true; }
|
|
86
|
+
}),
|
|
87
|
+
})).filter(entry => entry.methods.length > 0),
|
|
88
|
+
};
|
|
89
|
+
`,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.scriptError).toBeUndefined();
|
|
93
|
+
expect(result.returnValue).toEqual({
|
|
94
|
+
privateNamesPresent: [],
|
|
95
|
+
valuesWithReferenceMethods: [],
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("keeps every public function constructor in the isolate realm", async () => {
|
|
100
|
+
const result: ReturnResult = await runInSandbox({
|
|
101
|
+
code: `
|
|
102
|
+
const functions = [
|
|
103
|
+
console.log,
|
|
104
|
+
oneuptime.captureMetric,
|
|
105
|
+
axios,
|
|
106
|
+
axios.get,
|
|
107
|
+
crypto.randomUUID,
|
|
108
|
+
setTimeout,
|
|
109
|
+
sleep,
|
|
110
|
+
http.Agent,
|
|
111
|
+
https.Agent,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
directFunction: Function('return typeof process')(),
|
|
116
|
+
constructors: functions.map(fn =>
|
|
117
|
+
fn.constructor.constructor('return typeof process')()
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.scriptError).toBeUndefined();
|
|
124
|
+
expect(result.returnValue).toEqual({
|
|
125
|
+
directFunction: "undefined",
|
|
126
|
+
constructors: Array(9).fill("undefined"),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("keeps promises returned by async shims in the isolate realm", async () => {
|
|
131
|
+
const result: ReturnResult = await runInSandbox({
|
|
132
|
+
code: `
|
|
133
|
+
const sleepPromise = sleep(0);
|
|
134
|
+
const axiosPromise = axios.get('http://127.0.0.1:8080/');
|
|
135
|
+
const promiseRealms = [sleepPromise, axiosPromise].map(promise =>
|
|
136
|
+
promise.constructor.constructor('return typeof process')()
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await sleepPromise;
|
|
140
|
+
try { await axiosPromise; } catch (_) {}
|
|
141
|
+
return promiseRealms;
|
|
142
|
+
`,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.scriptError).toBeUndefined();
|
|
146
|
+
expect(result.returnValue).toEqual(["undefined", "undefined"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("deep-copies args into the isolate realm", async () => {
|
|
150
|
+
const args: JSONObject = {
|
|
151
|
+
nested: {
|
|
152
|
+
value: "host value",
|
|
153
|
+
},
|
|
154
|
+
items: [1, 2, 3],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const result: ReturnResult = await runInSandbox({
|
|
158
|
+
args,
|
|
159
|
+
code: `
|
|
160
|
+
args.nested.value = 'isolate value';
|
|
161
|
+
args.items.push(4);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
nestedValue: args.nested.value,
|
|
165
|
+
itemCount: args.items.length,
|
|
166
|
+
objectPrototypeIsLocal: Object.getPrototypeOf(args) === Object.prototype,
|
|
167
|
+
constructorRealm: args.constructor.constructor('return typeof process')(),
|
|
168
|
+
};
|
|
169
|
+
`,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.scriptError).toBeUndefined();
|
|
173
|
+
expect(result.returnValue).toEqual({
|
|
174
|
+
nestedValue: "isolate value",
|
|
175
|
+
itemCount: 4,
|
|
176
|
+
objectPrototypeIsLocal: true,
|
|
177
|
+
constructorRealm: "undefined",
|
|
178
|
+
});
|
|
179
|
+
expect(args).toEqual({
|
|
180
|
+
nested: {
|
|
181
|
+
value: "host value",
|
|
182
|
+
},
|
|
183
|
+
items: [1, 2, 3],
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("retains logs and metrics when user code throws", async () => {
|
|
188
|
+
const result: ReturnResult = await runInSandbox({
|
|
189
|
+
code: `
|
|
190
|
+
console.log('before throw', { safe: true });
|
|
191
|
+
oneuptime.captureMetric('sandbox_metric', 42, { region: 'test' });
|
|
192
|
+
const error = new Error('expected isolate failure');
|
|
193
|
+
error.untrustedProperty = 'must not cross';
|
|
194
|
+
throw error;
|
|
195
|
+
`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result.returnValue).toBeUndefined();
|
|
199
|
+
expect(result.logMessages).toEqual(['before throw {"safe":true}']);
|
|
200
|
+
expect(result.capturedMetrics).toEqual([
|
|
201
|
+
{
|
|
202
|
+
name: "sandbox_metric",
|
|
203
|
+
value: 42,
|
|
204
|
+
attributes: { region: "test" },
|
|
205
|
+
},
|
|
206
|
+
]);
|
|
207
|
+
expect(result.scriptError).toBeInstanceOf(Error);
|
|
208
|
+
expect(result.scriptError?.message).toBe("expected isolate failure");
|
|
209
|
+
expect(
|
|
210
|
+
(result.scriptError as Error & { untrustedProperty?: string })
|
|
211
|
+
.untrustedProperty,
|
|
212
|
+
).toBeUndefined();
|
|
213
|
+
expect(result.scriptError?.stack).not.toContain("untrustedProperty");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("retains logs and metrics when user code times out", async () => {
|
|
217
|
+
const result: ReturnResult = await runInSandbox({
|
|
218
|
+
timeout: 50,
|
|
219
|
+
code: `
|
|
220
|
+
console.log('before timeout');
|
|
221
|
+
oneuptime.captureMetric('timeout_metric', 7);
|
|
222
|
+
while (true) {}
|
|
223
|
+
`,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.returnValue).toBeUndefined();
|
|
227
|
+
expect(result.logMessages).toEqual(["before timeout"]);
|
|
228
|
+
expect(result.capturedMetrics).toEqual([
|
|
229
|
+
{
|
|
230
|
+
name: "timeout_metric",
|
|
231
|
+
value: 7,
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
expect(result.scriptError?.message).toMatch(/timed out/i);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("bounds log count and aggregate UTF-8 bytes", async () => {
|
|
238
|
+
const countResult: ReturnResult = await runInSandbox({
|
|
239
|
+
code: `
|
|
240
|
+
for (let index = 0; index < 1100; index++) {
|
|
241
|
+
console.log('entry-' + index);
|
|
242
|
+
}
|
|
243
|
+
return 'done';
|
|
244
|
+
`,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(countResult.scriptError).toBeUndefined();
|
|
248
|
+
expect(countResult.returnValue).toBe("done");
|
|
249
|
+
expect(countResult.logMessages).toHaveLength(1000);
|
|
250
|
+
|
|
251
|
+
const byteResult: ReturnResult = await runInSandbox({
|
|
252
|
+
code: `
|
|
253
|
+
const message = '\u00e9'.repeat(70000);
|
|
254
|
+
for (let index = 0; index < 10; index++) {
|
|
255
|
+
console.log(message);
|
|
256
|
+
}
|
|
257
|
+
return 'done';
|
|
258
|
+
`,
|
|
259
|
+
});
|
|
260
|
+
const totalBytes: number = byteResult.logMessages.reduce(
|
|
261
|
+
(bytes: number, message: string): number => {
|
|
262
|
+
return bytes + Buffer.byteLength(message, "utf8");
|
|
263
|
+
},
|
|
264
|
+
0,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(byteResult.scriptError).toBeUndefined();
|
|
268
|
+
expect(totalBytes).toBeLessThanOrEqual(1_000_000);
|
|
269
|
+
expect(byteResult.logMessages).toHaveLength(7);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("preserves timer behavior and clears pending host timers", async () => {
|
|
273
|
+
jest.spyOn(global, "clearTimeout");
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const result: ReturnResult = await runInSandbox({
|
|
277
|
+
timeout: 250,
|
|
278
|
+
code: `
|
|
279
|
+
let cancelledTimerFired = false;
|
|
280
|
+
const cancelledTimer = setTimeout(() => {
|
|
281
|
+
cancelledTimerFired = true;
|
|
282
|
+
}, 5);
|
|
283
|
+
clearTimeout(cancelledTimer);
|
|
284
|
+
|
|
285
|
+
const startedAt = Date.now();
|
|
286
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
287
|
+
const elapsed = Date.now() - startedAt;
|
|
288
|
+
|
|
289
|
+
setTimeout(() => console.log('late timer'), 60000);
|
|
290
|
+
await sleep(5);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
cancelledTimerFired,
|
|
294
|
+
elapsed,
|
|
295
|
+
};
|
|
296
|
+
`,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(result.scriptError).toBeUndefined();
|
|
300
|
+
expect(result.returnValue.cancelledTimerFired).toBe(false);
|
|
301
|
+
expect(result.returnValue.elapsed).toBeGreaterThanOrEqual(5);
|
|
302
|
+
expect(result.logMessages).not.toContain("late timer");
|
|
303
|
+
expect(
|
|
304
|
+
jest.mocked(global.clearTimeout).mock.calls.length,
|
|
305
|
+
).toBeGreaterThanOrEqual(2);
|
|
306
|
+
} finally {
|
|
307
|
+
jest.restoreAllMocks();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("copies a fulfilled axios response back into the isolate", async () => {
|
|
312
|
+
jest.spyOn(axios, "get").mockResolvedValue({
|
|
313
|
+
status: 200,
|
|
314
|
+
headers: { "x-test": "copied" },
|
|
315
|
+
data: { ok: true },
|
|
316
|
+
} as never);
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const result: ReturnResult = await runInSandbox({
|
|
320
|
+
code: `
|
|
321
|
+
const response = await axios.get('https://8.8.8.8/health');
|
|
322
|
+
return {
|
|
323
|
+
status: response.status,
|
|
324
|
+
header: response.headers['x-test'],
|
|
325
|
+
data: response.data,
|
|
326
|
+
promiseRealm: axios.get('https://8.8.8.8/second')
|
|
327
|
+
.constructor.constructor('return typeof process')(),
|
|
328
|
+
};
|
|
329
|
+
`,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(result.scriptError).toBeUndefined();
|
|
333
|
+
expect(result.returnValue).toEqual({
|
|
334
|
+
status: 200,
|
|
335
|
+
header: "copied",
|
|
336
|
+
data: { ok: true },
|
|
337
|
+
promiseRealm: "undefined",
|
|
338
|
+
});
|
|
339
|
+
expect(axios.get).toHaveBeenCalledWith(
|
|
340
|
+
"https://8.8.8.8/health",
|
|
341
|
+
expect.objectContaining({
|
|
342
|
+
maxRedirects: 0,
|
|
343
|
+
signal: expect.any(AbortSignal),
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
} finally {
|
|
347
|
+
jest.restoreAllMocks();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("keeps the documented crypto and Agent shims compatible", async () => {
|
|
352
|
+
const result: ReturnResult = await runInSandbox({
|
|
353
|
+
code: `
|
|
354
|
+
const httpAgent = new http.Agent({ keepAlive: true });
|
|
355
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
hash: crypto.createHash('sha256').update('oneuptime').digest('hex'),
|
|
359
|
+
uuidLooksValid: /^[0-9a-f-]{36}$/.test(crypto.randomUUID()),
|
|
360
|
+
randomIntInRange: (() => {
|
|
361
|
+
const value = crypto.randomInt(2, 5);
|
|
362
|
+
return value >= 2 && value < 5;
|
|
363
|
+
})(),
|
|
364
|
+
httpAgent,
|
|
365
|
+
httpsAgent,
|
|
366
|
+
};
|
|
367
|
+
`,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(result.scriptError).toBeUndefined();
|
|
371
|
+
expect(result.returnValue).toEqual({
|
|
372
|
+
hash: "35a18a92a1ae8387b173763364c1497950fa0bfe2589e545c0bf1f4d0f01f94e",
|
|
373
|
+
uuidLooksValid: true,
|
|
374
|
+
randomIntInRange: true,
|
|
375
|
+
httpAgent: {
|
|
376
|
+
__agentType: "__http_agent__",
|
|
377
|
+
options: { keepAlive: true },
|
|
378
|
+
},
|
|
379
|
+
httpsAgent: {
|
|
380
|
+
__agentType: "__https_agent__",
|
|
381
|
+
options: { rejectUnauthorized: false },
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
@@ -33,6 +33,9 @@ async function runInSandbox(code: string): Promise<ReturnResult> {
|
|
|
33
33
|
async function errorFromSandbox(code: string): Promise<string> {
|
|
34
34
|
try {
|
|
35
35
|
const result: ReturnResult = await runInSandbox(code);
|
|
36
|
+
if (result.scriptError) {
|
|
37
|
+
return result.scriptError.message;
|
|
38
|
+
}
|
|
36
39
|
return JSON.stringify(result.returnValue ?? result.logMessages ?? "");
|
|
37
40
|
} catch (err) {
|
|
38
41
|
return (err as Error).message || String(err);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
ACTIVE_MONITOR_PRICE_IN_USD_PER_MONTH,
|
|
4
|
+
PRICING_PAGE_URL,
|
|
5
|
+
SESSION_REPLAY_PRICE_IN_USD_PER_GB,
|
|
6
|
+
TELEMETRY_PRICE_IN_USD_PER_GB,
|
|
7
|
+
TELEMETRY_PRICE_RETENTION_IN_DAYS,
|
|
8
|
+
formatPriceInUSD,
|
|
9
|
+
} from "../../../Types/Billing/PayAsYouGoPricing";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* These constants are quoted to users, verbatim, on the telemetry ingestion
|
|
13
|
+
* key and create monitor pages. They are pinned here because changing one
|
|
14
|
+
* silently changes what the product promises a customer - a change to any of
|
|
15
|
+
* them has to be deliberate, and has to be made on the pricing page too.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
describe("PayAsYouGoPricing", () => {
|
|
19
|
+
describe("rates", () => {
|
|
20
|
+
it("charges $0.10 per GB of telemetry, quoted for 15 day retention", () => {
|
|
21
|
+
expect(TELEMETRY_PRICE_IN_USD_PER_GB).toBe(0.1);
|
|
22
|
+
expect(TELEMETRY_PRICE_RETENTION_IN_DAYS).toBe(15);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("charges $2 per GB for session replay, on the same 15 day window", () => {
|
|
26
|
+
expect(SESSION_REPLAY_PRICE_IN_USD_PER_GB).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("charges $1 per active monitor per month", () => {
|
|
30
|
+
expect(ACTIVE_MONITOR_PRICE_IN_USD_PER_MONTH).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("points at the public pricing page", () => {
|
|
34
|
+
expect(PRICING_PAGE_URL).toBe("https://oneuptime.com/pricing");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("formatPriceInUSD", () => {
|
|
39
|
+
it("writes a whole number of dollars without cents, like the pricing page", () => {
|
|
40
|
+
expect(formatPriceInUSD(1)).toBe("$1");
|
|
41
|
+
expect(formatPriceInUSD(22)).toBe("$22");
|
|
42
|
+
expect(formatPriceInUSD(0)).toBe("$0");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("writes a fractional amount with two decimal places", () => {
|
|
46
|
+
expect(formatPriceInUSD(0.1)).toBe("$0.10");
|
|
47
|
+
expect(formatPriceInUSD(1.5)).toBe("$1.50");
|
|
48
|
+
expect(formatPriceInUSD(2.05)).toBe("$2.05");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("rounds to cents rather than spilling extra digits into the copy", () => {
|
|
52
|
+
expect(formatPriceInUSD(0.125)).toBe("$0.13");
|
|
53
|
+
expect(formatPriceInUSD(0.006)).toBe("$0.01");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("renders the advertised rates exactly as the pricing page writes them", () => {
|
|
57
|
+
expect(formatPriceInUSD(TELEMETRY_PRICE_IN_USD_PER_GB)).toBe("$0.10");
|
|
58
|
+
expect(formatPriceInUSD(SESSION_REPLAY_PRICE_IN_USD_PER_GB)).toBe("$2");
|
|
59
|
+
expect(formatPriceInUSD(ACTIVE_MONITOR_PRICE_IN_USD_PER_MONTH)).toBe(
|
|
60
|
+
"$1",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
|
|
2
|
+
import FormFieldSchemaType from "../../../UI/Components/Forms/Types/FormFieldSchemaType";
|
|
3
|
+
import FieldType from "../../../UI/Components/Types/FieldType";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* CustomFieldsDetail does not translate a definition's `customFieldType` into a
|
|
8
|
+
* render type - it hands the raw string to a Detail field and to a form field
|
|
9
|
+
* as their `fieldType`, through an `as any` that erases the enum on the way.
|
|
10
|
+
* Nothing in the compiler is watching that seam, so a member added here without
|
|
11
|
+
* a same-VALUE member in FieldType and FormFieldSchemaType type-checks, builds,
|
|
12
|
+
* ships, and only then shows itself: the Detail column falls off the end of the
|
|
13
|
+
* render switch and paints an empty cell, and the form falls off the end of the
|
|
14
|
+
* input switch and paints a control the user cannot type into. The field looks
|
|
15
|
+
* present and is silently unusable, which is the worst shape a regression can
|
|
16
|
+
* take because nothing throws and nothing logs.
|
|
17
|
+
*
|
|
18
|
+
* These are value-set comparisons rather than key comparisons on purpose. The
|
|
19
|
+
* three enums are free to spell a member however reads best on their own side -
|
|
20
|
+
* `Boolean` reaches the form as `FormFieldSchemaType.Toggle`, which is written
|
|
21
|
+
* `Toggle = "Boolean"` for exactly this reason - and only the value crosses the
|
|
22
|
+
* seam. A key-based check would both miss real breakage and reject that legal
|
|
23
|
+
* spelling.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const customFieldTypeEntries: Array<[string, string]> = Object.entries(
|
|
27
|
+
CustomFieldType,
|
|
28
|
+
) as Array<[string, string]>;
|
|
29
|
+
|
|
30
|
+
const fieldTypeValues: Set<string> = new Set<string>(
|
|
31
|
+
Object.values(FieldType) as Array<string>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const formFieldSchemaTypeValues: Set<string> = new Set<string>(
|
|
35
|
+
Object.values(FormFieldSchemaType) as Array<string>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
describe("CustomFieldType corresponds to FieldType", () => {
|
|
39
|
+
test.each(customFieldTypeEntries)(
|
|
40
|
+
"CustomFieldType.%s is a FieldType value",
|
|
41
|
+
(_key: string, value: string) => {
|
|
42
|
+
expect(fieldTypeValues.has(value)).toBe(true);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("CustomFieldType corresponds to FormFieldSchemaType", () => {
|
|
48
|
+
test.each(customFieldTypeEntries)(
|
|
49
|
+
"CustomFieldType.%s is a FormFieldSchemaType value",
|
|
50
|
+
(_key: string, value: string) => {
|
|
51
|
+
expect(formFieldSchemaTypeValues.has(value)).toBe(true);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("CustomFieldType member set", () => {
|
|
57
|
+
test("has at least one member so the correspondence loops cannot pass vacuously", () => {
|
|
58
|
+
/*
|
|
59
|
+
* The two blocks above are generated from this enum. If it were ever
|
|
60
|
+
* emptied - a bad merge, a barrel file re-export that resolves to {} - the
|
|
61
|
+
* per-member assertions would simply stop existing and the suite would go
|
|
62
|
+
* green while guarding nothing.
|
|
63
|
+
*/
|
|
64
|
+
expect(customFieldTypeEntries.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("has no duplicate values", () => {
|
|
68
|
+
/*
|
|
69
|
+
* Two keys sharing a value collapse at runtime: the second one wins every
|
|
70
|
+
* lookup, and a definition saved under the first key renders as the second
|
|
71
|
+
* one's type. It also makes the loops above lie, because a duplicate is
|
|
72
|
+
* checked twice and the shadowed member is never exercised on its own.
|
|
73
|
+
*/
|
|
74
|
+
const values: Array<string> = customFieldTypeEntries.map(
|
|
75
|
+
([, value]: [string, string]) => {
|
|
76
|
+
return value;
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(new Set<string>(values).size).toBe(values.length);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("includes Date and DateTime", () => {
|
|
84
|
+
/*
|
|
85
|
+
* The date members are what the jsonb range filters and the ISO-8601
|
|
86
|
+
* storage format exist for. Losing them is a silent feature removal - old
|
|
87
|
+
* definitions keep their stored `customFieldType` string in the database
|
|
88
|
+
* and would then fall through every switch that reads this enum.
|
|
89
|
+
*/
|
|
90
|
+
expect(CustomFieldType.Date).toEqual("Date");
|
|
91
|
+
expect(CustomFieldType.DateTime).toEqual("DateTime");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("CustomFieldType correspondence is by value, not by key", () => {
|
|
96
|
+
test("Boolean reaches the form as Toggle, which is only visible by value", () => {
|
|
97
|
+
/*
|
|
98
|
+
* This is the case that proves the checks above have to compare values.
|
|
99
|
+
* FormFieldSchemaType has no member named `Boolean`; it names the same
|
|
100
|
+
* value `Toggle`. A correspondence test written against key names would
|
|
101
|
+
* fail here on code that works perfectly, and would then be "fixed" by
|
|
102
|
+
* relaxing it into something that no longer catches a missing member.
|
|
103
|
+
*/
|
|
104
|
+
expect(Object.keys(FormFieldSchemaType)).not.toContain("Boolean");
|
|
105
|
+
expect(FormFieldSchemaType.Toggle).toEqual(CustomFieldType.Boolean);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -253,6 +253,68 @@ describe("MonitorTypeHelper", () => {
|
|
|
253
253
|
});
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
+
/*
|
|
257
|
+
* The dashboard tells Free plan users which monitors cost money before they
|
|
258
|
+
* create one, and gates the create button on acknowledging it. That warning
|
|
259
|
+
* is only honest while this predicate agrees with what the server actually
|
|
260
|
+
* meters - ActiveMonitoringMeteredPlan counts every monitor whose type is
|
|
261
|
+
* not Manual.
|
|
262
|
+
*/
|
|
263
|
+
describe("isBilledAsActiveMonitor", () => {
|
|
264
|
+
test("Manual monitors are free", () => {
|
|
265
|
+
expect(
|
|
266
|
+
MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Manual),
|
|
267
|
+
).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("every other monitor type is billed", () => {
|
|
271
|
+
const billed: Array<MonitorType> = Object.values(MonitorType).filter(
|
|
272
|
+
(monitorType: MonitorType) => {
|
|
273
|
+
return monitorType !== MonitorType.Manual;
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(billed.length).toBeGreaterThan(0);
|
|
278
|
+
|
|
279
|
+
for (const monitorType of billed) {
|
|
280
|
+
expect(MonitorTypeHelper.isBilledAsActiveMonitor(monitorType)).toBe(
|
|
281
|
+
true,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("telemetry monitors are billed as active monitors too, on top of their ingest", () => {
|
|
287
|
+
expect(MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Logs)).toBe(
|
|
288
|
+
true,
|
|
289
|
+
);
|
|
290
|
+
expect(
|
|
291
|
+
MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Metrics),
|
|
292
|
+
).toBe(true);
|
|
293
|
+
expect(
|
|
294
|
+
MonitorTypeHelper.isBilledAsActiveMonitor(MonitorType.Traces),
|
|
295
|
+
).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("agrees exactly with getActiveMonitorTypes", () => {
|
|
299
|
+
const active: Array<MonitorType> =
|
|
300
|
+
MonitorTypeHelper.getActiveMonitorTypes();
|
|
301
|
+
|
|
302
|
+
for (const monitorType of Object.values(MonitorType)) {
|
|
303
|
+
expect(MonitorTypeHelper.isBilledAsActiveMonitor(monitorType)).toBe(
|
|
304
|
+
active.includes(monitorType),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("is the exact inverse of isManualMonitor", () => {
|
|
310
|
+
for (const monitorType of Object.values(MonitorType)) {
|
|
311
|
+
expect(MonitorTypeHelper.isBilledAsActiveMonitor(monitorType)).toBe(
|
|
312
|
+
!MonitorTypeHelper.isManualMonitor(monitorType),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
256
318
|
describe("doesMonitorTypeHaveGraphs", () => {
|
|
257
319
|
test("returns true for graphable types and false otherwise", () => {
|
|
258
320
|
expect(
|