@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.
Files changed (98) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  6. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  7. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  8. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
  9. package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
  10. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
  11. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  12. package/Server/Utils/VM/VMRunner.ts +693 -874
  13. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  14. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  15. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  16. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  17. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  18. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  19. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  20. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  21. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  22. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  23. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  24. package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
  25. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
  26. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  27. package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
  28. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
  29. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  30. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  31. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  32. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  33. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  34. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  35. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  36. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  37. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  38. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  39. package/Types/CustomField/CustomFieldType.ts +28 -0
  40. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
  41. package/Types/Monitor/MonitorType.ts +11 -0
  42. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  43. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  44. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  45. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  46. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  47. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  48. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  49. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  50. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  51. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  52. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  53. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  55. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  60. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  61. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  62. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  63. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  64. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  65. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  66. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
  67. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  68. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  69. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  70. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
  71. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  72. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  73. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  74. package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
  75. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  76. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  77. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  78. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  79. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  80. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  81. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  82. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  83. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  84. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  85. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  86. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  87. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  88. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  89. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  90. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  91. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  92. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  93. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  94. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  95. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  96. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  97. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  98. 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(