@oneuptime/common 11.6.2 → 11.7.1

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 (64) hide show
  1. package/Models/AnalyticsModels/SloHistory.ts +6 -3
  2. package/Models/DatabaseModels/ServiceLevelObjective.ts +9 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1785066759532-MigrationName.ts +30 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/ServiceLevelObjectiveService.ts +12 -4
  6. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +2 -2
  7. package/Tests/Types/DateUserTimezone.test.ts +205 -0
  8. package/Tests/Types/DateUserTimezoneOverridesBrowser.test.ts +73 -0
  9. package/Tests/Types/Monitor/MonitorCriteria.test.ts +155 -0
  10. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +465 -0
  11. package/Tests/Types/Monitor/MonitorStep.test.ts +183 -0
  12. package/Tests/Types/Monitor/MonitorSteps.test.ts +183 -0
  13. package/Tests/UI/Components/TimePicker/TimePicker.test.tsx +11 -0
  14. package/Tests/Utils/Memory.test.ts +44 -0
  15. package/Tests/Utils/Slo/SloBurnRateRuleState.test.ts +124 -0
  16. package/Tests/Utils/Slo/SloDuration.test.ts +148 -0
  17. package/Tests/Utils/Slo/SloHealth.test.ts +334 -0
  18. package/Types/Date.ts +119 -50
  19. package/UI/Components/Forms/Fields/FormField.tsx +13 -3
  20. package/UI/Components/Input/Input.tsx +6 -1
  21. package/UI/Components/TimePicker/TimePicker.tsx +8 -4
  22. package/UI/Utils/User.ts +19 -0
  23. package/Utils/Slo/SloBurnRateRuleState.ts +91 -0
  24. package/Utils/Slo/SloDuration.ts +165 -0
  25. package/Utils/Slo/SloEvaluation.ts +22 -0
  26. package/Utils/Slo/SloHealth.ts +302 -0
  27. package/build/dist/Models/AnalyticsModels/SloHistory.js +6 -3
  28. package/build/dist/Models/AnalyticsModels/SloHistory.js.map +1 -1
  29. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +11 -1
  30. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785066759532-MigrationName.js +21 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785066759532-MigrationName.js.map +1 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  35. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +10 -4
  36. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  37. package/build/dist/Types/Date.js +106 -35
  38. package/build/dist/Types/Date.js.map +1 -1
  39. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -3
  40. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  41. package/build/dist/UI/Components/Input/Input.js +6 -1
  42. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  43. package/build/dist/UI/Components/TimePicker/TimePicker.js +4 -4
  44. package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
  45. package/build/dist/UI/Utils/User.js +19 -0
  46. package/build/dist/UI/Utils/User.js.map +1 -1
  47. package/build/dist/Utils/Slo/SloBurnRateRuleState.js +35 -0
  48. package/build/dist/Utils/Slo/SloBurnRateRuleState.js.map +1 -0
  49. package/build/dist/Utils/Slo/SloDuration.js +87 -0
  50. package/build/dist/Utils/Slo/SloDuration.js.map +1 -0
  51. package/build/dist/Utils/Slo/SloEvaluation.js +21 -0
  52. package/build/dist/Utils/Slo/SloEvaluation.js.map +1 -0
  53. package/build/dist/Utils/Slo/SloHealth.js +179 -0
  54. package/build/dist/Utils/Slo/SloHealth.js.map +1 -0
  55. package/package.json +1 -1
  56. package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +0 -79
  57. package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +0 -41
  58. package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +0 -17
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +0 -34
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +0 -1
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +0 -22
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +0 -1
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +0 -12
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +0 -1
@@ -0,0 +1,465 @@
1
+ import MonitorCriteriaInstance from "../../../Types/Monitor/MonitorCriteriaInstance";
2
+ import MonitorType from "../../../Types/Monitor/MonitorType";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import FilterCondition from "../../../Types/Filter/FilterCondition";
5
+ import {
6
+ CheckOn,
7
+ CriteriaFilter,
8
+ FilterType,
9
+ } from "../../../Types/Monitor/CriteriaFilter";
10
+ import { JSONObject, ObjectType } from "../../../Types/JSON";
11
+ import BadDataException from "../../../Types/Exception/BadDataException";
12
+
13
+ /*
14
+ * MonitorCriteriaInstance is a DatabaseProperty that carries the "if these
15
+ * filters match, then change status / open incidents / fire alerts" rule for a
16
+ * single monitor status. The contract these tests lock in:
17
+ *
18
+ * 1. Factory defaults (getNewMonitorCriteriaInstanceAsJSON, and the
19
+ * per-monitor-type online/offline builders) produce well-formed criteria.
20
+ * 2. getValidationError catches every missing-required-field case, and
21
+ * returns null only for a fully valid instance.
22
+ * 3. toJSON <-> fromJSON is a lossless round-trip, fromJSON rejects malformed
23
+ * payloads, and clone() is a deep copy built on that round-trip.
24
+ */
25
+
26
+ // A minimal, valid criteria instance for validation / serialization tests.
27
+ const buildValidInstance: () => MonitorCriteriaInstance =
28
+ (): MonitorCriteriaInstance => {
29
+ const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance();
30
+ instance.data = {
31
+ id: ObjectID.generate().toString(),
32
+ monitorStatusId: new ObjectID("statusid1234567890abcdef"),
33
+ filterCondition: FilterCondition.All,
34
+ filters: [
35
+ {
36
+ checkOn: CheckOn.IsOnline,
37
+ filterType: FilterType.True,
38
+ value: undefined,
39
+ },
40
+ ],
41
+ incidents: [],
42
+ alerts: [],
43
+ createIncidents: false,
44
+ createAlerts: false,
45
+ changeMonitorStatus: true,
46
+ isEnabled: true,
47
+ name: "Online check",
48
+ description: "Checks whether the monitor is online",
49
+ };
50
+ return instance;
51
+ };
52
+
53
+ describe("MonitorCriteriaInstance", () => {
54
+ describe("constructor", () => {
55
+ test("seeds a default IsOnline filter and a fresh id", () => {
56
+ const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance();
57
+ expect(instance.data).toBeDefined();
58
+ expect(instance.data?.filterCondition).toBe(FilterCondition.All);
59
+ expect(instance.data?.filters).toHaveLength(1);
60
+ expect(instance.data?.filters[0]?.checkOn).toBe(CheckOn.IsOnline);
61
+ expect(instance.data?.isEnabled).toBe(true);
62
+ // id should be a valid, non-empty ObjectID string.
63
+ expect(typeof instance.data?.id).toBe("string");
64
+ expect(instance.data?.id.length).toBeGreaterThan(0);
65
+ });
66
+
67
+ test("two instances get distinct ids", () => {
68
+ const a: MonitorCriteriaInstance = new MonitorCriteriaInstance();
69
+ const b: MonitorCriteriaInstance = new MonitorCriteriaInstance();
70
+ expect(a.data?.id).not.toBe(b.data?.id);
71
+ });
72
+ });
73
+
74
+ describe("getNewMonitorCriteriaInstanceAsJSON", () => {
75
+ test("returns default JSON with an IsOnline/True filter", () => {
76
+ const json: JSONObject =
77
+ MonitorCriteriaInstance.getNewMonitorCriteriaInstanceAsJSON();
78
+ expect(json["filterCondition"]).toBe(FilterCondition.All);
79
+ expect(json["name"]).toBe("");
80
+ expect(json["description"]).toBe("");
81
+ expect(json["createIncidents"]).toBe(false);
82
+ expect(json["changeMonitorStatus"]).toBe(false);
83
+ expect(Array.isArray(json["filters"])).toBe(true);
84
+ const filters: Array<CriteriaFilter> = json[
85
+ "filters"
86
+ ] as unknown as Array<CriteriaFilter>;
87
+ expect(filters[0]?.checkOn).toBe(CheckOn.IsOnline);
88
+ expect(filters[0]?.filterType).toBe(FilterType.True);
89
+ });
90
+ });
91
+
92
+ describe("getDefaultOnlineMonitorCriteriaInstance", () => {
93
+ const monitorStatusId: ObjectID = new ObjectID("aaaaaaaaaaaaaaaaaaaaaaaa");
94
+
95
+ test("Ping monitor produces an IsOnline online criteria", () => {
96
+ const instance: MonitorCriteriaInstance | null =
97
+ MonitorCriteriaInstance.getDefaultOnlineMonitorCriteriaInstance({
98
+ monitorType: MonitorType.Ping,
99
+ monitorStatusId,
100
+ monitorName: "My Server",
101
+ });
102
+ expect(instance).not.toBeNull();
103
+ expect(instance?.data?.changeMonitorStatus).toBe(true);
104
+ expect(instance?.data?.filters[0]?.checkOn).toBe(CheckOn.IsOnline);
105
+ expect(instance?.data?.name).toContain("My Server");
106
+ expect(instance?.data?.monitorStatusId?.toString()).toBe(
107
+ monitorStatusId.toString(),
108
+ );
109
+ });
110
+
111
+ test("IncomingRequest monitor uses a RecievedInMinutes filter", () => {
112
+ const instance: MonitorCriteriaInstance | null =
113
+ MonitorCriteriaInstance.getDefaultOnlineMonitorCriteriaInstance({
114
+ monitorType: MonitorType.IncomingRequest,
115
+ monitorStatusId,
116
+ monitorName: "Heartbeat",
117
+ });
118
+ expect(instance).not.toBeNull();
119
+ expect(instance?.data?.filters[0]?.checkOn).toBe(CheckOn.IncomingRequest);
120
+ expect(instance?.data?.filters[0]?.filterType).toBe(
121
+ FilterType.RecievedInMinutes,
122
+ );
123
+ expect(instance?.data?.filters[0]?.value).toBe(30);
124
+ });
125
+
126
+ test("Metrics monitor threads the first metric alias into options", () => {
127
+ const instance: MonitorCriteriaInstance | null =
128
+ MonitorCriteriaInstance.getDefaultOnlineMonitorCriteriaInstance({
129
+ monitorType: MonitorType.Metrics,
130
+ monitorStatusId,
131
+ monitorName: "CPU",
132
+ metricOptions: { metricAliases: ["cpu_usage", "mem_usage"] },
133
+ });
134
+ expect(instance).not.toBeNull();
135
+ expect(
136
+ instance?.data?.filters[0]?.metricMonitorOptions?.metricAlias,
137
+ ).toBe("cpu_usage");
138
+ });
139
+
140
+ test("Manual monitor type has no online criteria (returns null)", () => {
141
+ const instance: MonitorCriteriaInstance | null =
142
+ MonitorCriteriaInstance.getDefaultOnlineMonitorCriteriaInstance({
143
+ monitorType: MonitorType.Manual,
144
+ monitorStatusId,
145
+ monitorName: "Manual",
146
+ });
147
+ expect(instance).toBeNull();
148
+ });
149
+ });
150
+
151
+ describe("getDefaultOfflineMonitorCriteriaInstance", () => {
152
+ test("Ping offline criteria populates incident and alert with severities", () => {
153
+ const monitorStatusId: ObjectID = new ObjectID(
154
+ "bbbbbbbbbbbbbbbbbbbbbbbb",
155
+ );
156
+ const incidentSeverityId: ObjectID = new ObjectID(
157
+ "cccccccccccccccccccccccc",
158
+ );
159
+ const alertSeverityId: ObjectID = new ObjectID(
160
+ "dddddddddddddddddddddddd",
161
+ );
162
+
163
+ const instance: MonitorCriteriaInstance =
164
+ MonitorCriteriaInstance.getDefaultOfflineMonitorCriteriaInstance({
165
+ monitorType: MonitorType.Ping,
166
+ monitorStatusId,
167
+ incidentSeverityId,
168
+ alertSeverityId,
169
+ monitorName: "Edge",
170
+ });
171
+
172
+ expect(instance.data?.createIncidents).toBe(true);
173
+ expect(instance.data?.changeMonitorStatus).toBe(true);
174
+ expect(instance.data?.filters[0]?.checkOn).toBe(CheckOn.IsOnline);
175
+ expect(instance.data?.filters[0]?.filterType).toBe(FilterType.False);
176
+ expect(instance.data?.incidents[0]?.incidentSeverityId?.toString()).toBe(
177
+ incidentSeverityId.toString(),
178
+ );
179
+ expect(instance.data?.alerts[0]?.alertSeverityId?.toString()).toBe(
180
+ alertSeverityId.toString(),
181
+ );
182
+ expect(instance.data?.incidents[0]?.title).toContain("Edge");
183
+ });
184
+ });
185
+
186
+ describe("getValidationError", () => {
187
+ test("returns error when data is missing", () => {
188
+ const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance();
189
+ instance.data = undefined;
190
+ expect(
191
+ MonitorCriteriaInstance.getValidationError(instance, MonitorType.Ping),
192
+ ).toBe("Monitor Step is required.");
193
+ });
194
+
195
+ test("returns error when there are no filters", () => {
196
+ const instance: MonitorCriteriaInstance = buildValidInstance();
197
+ instance.data!.filters = [];
198
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
199
+ instance,
200
+ MonitorType.Ping,
201
+ );
202
+ expect(error).toContain("Filter is required");
203
+ });
204
+
205
+ test("returns error when name is empty", () => {
206
+ const instance: MonitorCriteriaInstance = buildValidInstance();
207
+ instance.data!.name = "";
208
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
209
+ instance,
210
+ MonitorType.Ping,
211
+ );
212
+ expect(error).toContain("Name is required");
213
+ });
214
+
215
+ test("returns error when description is empty", () => {
216
+ const instance: MonitorCriteriaInstance = buildValidInstance();
217
+ instance.data!.description = "";
218
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
219
+ instance,
220
+ MonitorType.Ping,
221
+ );
222
+ expect(error).toContain("Description is required");
223
+ });
224
+
225
+ test("returns error when an incident is missing its severity", () => {
226
+ const instance: MonitorCriteriaInstance = buildValidInstance();
227
+ instance.data!.incidents = [
228
+ {
229
+ title: "Down",
230
+ description: "It is down",
231
+ incidentSeverityId: undefined,
232
+ id: ObjectID.generate().toString(),
233
+ onCallPolicyIds: [],
234
+ },
235
+ ];
236
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
237
+ instance,
238
+ MonitorType.Ping,
239
+ );
240
+ expect(error).toContain("Incident severity is required");
241
+ });
242
+
243
+ test("returns error when an alert is missing its title", () => {
244
+ const instance: MonitorCriteriaInstance = buildValidInstance();
245
+ instance.data!.alerts = [
246
+ {
247
+ title: "",
248
+ description: "It is down",
249
+ alertSeverityId: new ObjectID("eeeeeeeeeeeeeeeeeeeeeeee"),
250
+ id: ObjectID.generate().toString(),
251
+ onCallPolicyIds: [],
252
+ },
253
+ ];
254
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
255
+ instance,
256
+ MonitorType.Ping,
257
+ );
258
+ expect(error).toContain("Alert title is required");
259
+ });
260
+
261
+ test("rejects a filter type that a Ping monitor cannot support", () => {
262
+ const instance: MonitorCriteriaInstance = buildValidInstance();
263
+ instance.data!.filters = [
264
+ {
265
+ checkOn: CheckOn.DiskUsagePercent,
266
+ filterType: FilterType.GreaterThan,
267
+ value: 90,
268
+ },
269
+ ];
270
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
271
+ instance,
272
+ MonitorType.Ping,
273
+ );
274
+ expect(error).toContain("Ping Monitor cannot have filter type");
275
+ });
276
+
277
+ test("requires a disk path for Disk Usage Percent filters", () => {
278
+ const instance: MonitorCriteriaInstance = buildValidInstance();
279
+ instance.data!.filters = [
280
+ {
281
+ checkOn: CheckOn.DiskUsagePercent,
282
+ filterType: FilterType.GreaterThan,
283
+ value: 90,
284
+ },
285
+ ];
286
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
287
+ instance,
288
+ MonitorType.Server,
289
+ );
290
+ expect(error).toBe("Disk Path is required for Disk Usage Percent");
291
+ });
292
+
293
+ test("requires a value for filters that carry a threshold", () => {
294
+ const instance: MonitorCriteriaInstance = buildValidInstance();
295
+ instance.data!.filters = [
296
+ {
297
+ checkOn: CheckOn.ResponseTime,
298
+ filterType: FilterType.GreaterThan,
299
+ value: undefined,
300
+ },
301
+ ];
302
+ const error: string | null = MonitorCriteriaInstance.getValidationError(
303
+ instance,
304
+ MonitorType.Ping,
305
+ );
306
+ expect(error).toContain("Value is required");
307
+ });
308
+
309
+ test("returns null for a fully valid instance", () => {
310
+ const instance: MonitorCriteriaInstance = buildValidInstance();
311
+ expect(
312
+ MonitorCriteriaInstance.getValidationError(instance, MonitorType.Ping),
313
+ ).toBeNull();
314
+ });
315
+ });
316
+
317
+ describe("toJSON / fromJSON round-trip", () => {
318
+ test("serializes with the MonitorCriteriaInstance object type wrapper", () => {
319
+ const instance: MonitorCriteriaInstance = buildValidInstance();
320
+ const json: JSONObject = instance.toJSON();
321
+ expect(json["_type"]).toBe(ObjectType.MonitorCriteriaInstance);
322
+ expect(json["value"]).toBeDefined();
323
+ });
324
+
325
+ test("round-trips core fields losslessly", () => {
326
+ const instance: MonitorCriteriaInstance = buildValidInstance();
327
+ instance.data!.name = "Round Trip";
328
+ instance.data!.description = "A description";
329
+ instance.data!.changeMonitorStatus = true;
330
+ instance.data!.createIncidents = false;
331
+
332
+ const restored: MonitorCriteriaInstance =
333
+ MonitorCriteriaInstance.fromJSON(instance.toJSON());
334
+
335
+ expect(restored.data?.name).toBe("Round Trip");
336
+ expect(restored.data?.description).toBe("A description");
337
+ expect(restored.data?.filterCondition).toBe(FilterCondition.All);
338
+ expect(restored.data?.filters[0]?.checkOn).toBe(CheckOn.IsOnline);
339
+ expect(restored.data?.changeMonitorStatus).toBe(true);
340
+ expect(restored.data?.monitorStatusId?.toString()).toBe(
341
+ instance.data?.monitorStatusId?.toString(),
342
+ );
343
+ });
344
+
345
+ test("fromJSON defaults isEnabled to true when absent", () => {
346
+ const instance: MonitorCriteriaInstance = buildValidInstance();
347
+ const json: JSONObject = instance.toJSON();
348
+ // Simulate a legacy payload that predates the isEnabled field.
349
+ delete (json["value"] as JSONObject)["isEnabled"];
350
+ const restored: MonitorCriteriaInstance =
351
+ MonitorCriteriaInstance.fromJSON(json);
352
+ expect(restored.data?.isEnabled).toBe(true);
353
+ });
354
+
355
+ test("fromJSON is idempotent when given an existing instance", () => {
356
+ const instance: MonitorCriteriaInstance = buildValidInstance();
357
+ const same: MonitorCriteriaInstance = MonitorCriteriaInstance.fromJSON(
358
+ instance as unknown as JSONObject,
359
+ );
360
+ expect(same).toBe(instance);
361
+ });
362
+ });
363
+
364
+ describe("fromJSON validation", () => {
365
+ test("throws when _type is missing", () => {
366
+ expect(() => {
367
+ return MonitorCriteriaInstance.fromJSON({ value: {} });
368
+ }).toThrow(BadDataException);
369
+ });
370
+
371
+ test("throws when _type is wrong", () => {
372
+ expect(() => {
373
+ return MonitorCriteriaInstance.fromJSON({
374
+ _type: "SomethingElse",
375
+ value: {},
376
+ });
377
+ }).toThrow("json._type should be MonitorCriteriaInstance");
378
+ });
379
+
380
+ test("throws when value is missing", () => {
381
+ expect(() => {
382
+ return MonitorCriteriaInstance.fromJSON({
383
+ _type: ObjectType.MonitorCriteriaInstance,
384
+ });
385
+ }).toThrow("json.value is null");
386
+ });
387
+
388
+ test("throws when filterCondition is missing", () => {
389
+ expect(() => {
390
+ return MonitorCriteriaInstance.fromJSON({
391
+ _type: ObjectType.MonitorCriteriaInstance,
392
+ value: { filters: [] },
393
+ });
394
+ }).toThrow("json.filterCondition is null");
395
+ });
396
+
397
+ test("throws when filters is missing", () => {
398
+ expect(() => {
399
+ return MonitorCriteriaInstance.fromJSON({
400
+ _type: ObjectType.MonitorCriteriaInstance,
401
+ value: { filterCondition: FilterCondition.All },
402
+ });
403
+ }).toThrow("json.filters is null");
404
+ });
405
+
406
+ test("throws when filters is not an array", () => {
407
+ expect(() => {
408
+ return MonitorCriteriaInstance.fromJSON({
409
+ _type: ObjectType.MonitorCriteriaInstance,
410
+ value: {
411
+ filterCondition: FilterCondition.All,
412
+ filters: "not-an-array",
413
+ },
414
+ });
415
+ }).toThrow("json.filters should be an array");
416
+ });
417
+ });
418
+
419
+ describe("clone", () => {
420
+ test("produces a deep, independent copy that carries equal data", () => {
421
+ const instance: MonitorCriteriaInstance = buildValidInstance();
422
+ instance.data!.name = "Original";
423
+
424
+ const cloned: MonitorCriteriaInstance =
425
+ MonitorCriteriaInstance.clone(instance);
426
+
427
+ expect(cloned).not.toBe(instance);
428
+ expect(cloned.data?.name).toBe("Original");
429
+
430
+ // Mutating the clone must not affect the original.
431
+ cloned.data!.name = "Mutated";
432
+ expect(instance.data?.name).toBe("Original");
433
+ });
434
+ });
435
+
436
+ describe("setters", () => {
437
+ test("chain and mutate the underlying data", () => {
438
+ const statusId: ObjectID = new ObjectID("ffffffffffffffffffffffff");
439
+ const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance()
440
+ .setName("Chained")
441
+ .setDescription("Chained description")
442
+ .setChangeMonitorStatus(true)
443
+ .setCreateIncidents(true)
444
+ .setCreateAlerts(false)
445
+ .setIsEnabled(false)
446
+ .setMonitorStatusId(statusId);
447
+
448
+ expect(instance.data?.name).toBe("Chained");
449
+ expect(instance.data?.description).toBe("Chained description");
450
+ expect(instance.data?.changeMonitorStatus).toBe(true);
451
+ expect(instance.data?.createIncidents).toBe(true);
452
+ expect(instance.data?.createAlerts).toBe(false);
453
+ expect(instance.data?.isEnabled).toBe(false);
454
+ expect(instance.data?.monitorStatusId?.toString()).toBe(
455
+ statusId.toString(),
456
+ );
457
+ });
458
+ });
459
+
460
+ describe("isValid", () => {
461
+ test("returns true (permissive by contract)", () => {
462
+ expect(MonitorCriteriaInstance.isValid({})).toBe(true);
463
+ });
464
+ });
465
+ });
@@ -0,0 +1,183 @@
1
+ import MonitorStep from "../../../Types/Monitor/MonitorStep";
2
+ import MonitorType from "../../../Types/Monitor/MonitorType";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import HTTPMethod from "../../../Types/API/HTTPMethod";
5
+ import { JSONObject, ObjectType } from "../../../Types/JSON";
6
+
7
+ /*
8
+ * MonitorStep is the per-step config of a monitor (destination, request shape,
9
+ * TLS material, per-protocol sub-configs and the nested MonitorCriteria).
10
+ * These tests focus on the parts with real branching logic: getValidationError
11
+ * (which is monitor-type dependent), the factory defaults, clone, the id
12
+ * accessor, the fluent request setters and the toJSON/fromJSON round-trip.
13
+ */
14
+
15
+ const DEFAULT_ARG: {
16
+ monitorName: string;
17
+ monitorType: MonitorType;
18
+ onlineMonitorStatusId: ObjectID;
19
+ offlineMonitorStatusId: ObjectID;
20
+ defaultIncidentSeverityId: ObjectID;
21
+ defaultAlertSeverityId: ObjectID;
22
+ } = {
23
+ monitorName: "Web",
24
+ monitorType: MonitorType.Manual,
25
+ onlineMonitorStatusId: new ObjectID("100000000000000000000021"),
26
+ offlineMonitorStatusId: new ObjectID("100000000000000000000022"),
27
+ defaultIncidentSeverityId: new ObjectID("100000000000000000000023"),
28
+ defaultAlertSeverityId: new ObjectID("100000000000000000000024"),
29
+ };
30
+
31
+ describe("MonitorStep", () => {
32
+ describe("getNewMonitorStepAsJSON", () => {
33
+ test("carries a fresh id, GET request type and nested criteria", () => {
34
+ const json: JSONObject = MonitorStep.getNewMonitorStepAsJSON();
35
+ expect(json["_type"]).toBe(ObjectType.MonitorStep);
36
+ const value: JSONObject = json["value"] as JSONObject;
37
+ expect(typeof value["id"]).toBe("string");
38
+ expect(value["requestType"]).toBe(HTTPMethod.GET);
39
+ expect(value["monitorCriteria"]).toBeDefined();
40
+ });
41
+ });
42
+
43
+ describe("getDefaultMonitorStep", () => {
44
+ test("populates a fresh id, GET request type and nested criteria", () => {
45
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep(DEFAULT_ARG);
46
+ expect(typeof step.data?.id).toBe("string");
47
+ expect(step.data?.id?.length).toBeGreaterThan(0);
48
+ expect(step.data?.requestType).toBe(HTTPMethod.GET);
49
+ expect(step.data?.monitorCriteria).toBeDefined();
50
+ // The default destination is left for the user to fill in.
51
+ expect(step.data?.monitorDestination).toBeUndefined();
52
+ });
53
+ });
54
+
55
+ describe("getValidationError", () => {
56
+ test("errors when data is missing", () => {
57
+ const step: MonitorStep = new MonitorStep();
58
+ step.data = undefined;
59
+ expect(MonitorStep.getValidationError(step, MonitorType.Ping)).toBe(
60
+ "Monitor Step is required.",
61
+ );
62
+ });
63
+
64
+ test("requires a destination for a Ping monitor", () => {
65
+ // A default Ping step leaves monitorDestination undefined.
66
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
67
+ ...DEFAULT_ARG,
68
+ monitorType: MonitorType.Ping,
69
+ });
70
+ expect(MonitorStep.getValidationError(step, MonitorType.Ping)).toBe(
71
+ "Monitor Destination is required.",
72
+ );
73
+ });
74
+
75
+ test("requires a port for a Port monitor that has a destination", () => {
76
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
77
+ ...DEFAULT_ARG,
78
+ monitorType: MonitorType.Port,
79
+ });
80
+ /*
81
+ * Give it a destination so we exercise the missing-port branch, not the
82
+ * missing-destination branch that precedes it.
83
+ */
84
+ step.data!.monitorDestination = {
85
+ toJSON: () => {
86
+ return "1.2.3.4";
87
+ },
88
+ } as never;
89
+ expect(MonitorStep.getValidationError(step, MonitorType.Port)).toBe(
90
+ "Port is required",
91
+ );
92
+ });
93
+
94
+ test("rejects a client certificate without its key on an API monitor", () => {
95
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
96
+ ...DEFAULT_ARG,
97
+ monitorType: MonitorType.API,
98
+ });
99
+ step.data!.monitorDestination = {
100
+ toJSON: () => {
101
+ return "https://x";
102
+ },
103
+ } as never;
104
+ step.data!.requestType = HTTPMethod.GET;
105
+ step.data!.tlsClientCertificate = "----CERT----";
106
+ step.data!.tlsClientKey = undefined;
107
+ expect(MonitorStep.getValidationError(step, MonitorType.API)).toBe(
108
+ "Client private key is required when a client certificate is provided",
109
+ );
110
+ });
111
+
112
+ test("rejects a client key without its certificate on an API monitor", () => {
113
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep({
114
+ ...DEFAULT_ARG,
115
+ monitorType: MonitorType.API,
116
+ });
117
+ step.data!.monitorDestination = {
118
+ toJSON: () => {
119
+ return "https://x";
120
+ },
121
+ } as never;
122
+ step.data!.requestType = HTTPMethod.GET;
123
+ step.data!.tlsClientCertificate = undefined;
124
+ step.data!.tlsClientKey = "----KEY----";
125
+ expect(MonitorStep.getValidationError(step, MonitorType.API)).toBe(
126
+ "Client certificate is required when a client private key is provided",
127
+ );
128
+ });
129
+ });
130
+
131
+ describe("id accessor", () => {
132
+ test("get/set round-trips through the underlying string", () => {
133
+ const step: MonitorStep = new MonitorStep();
134
+ const newId: ObjectID = new ObjectID("100000000000000000000099");
135
+ step.id = newId;
136
+ expect(step.data?.id).toBe("100000000000000000000099");
137
+ expect(step.id.toString()).toBe("100000000000000000000099");
138
+ });
139
+ });
140
+
141
+ describe("request setters", () => {
142
+ test("chain and mutate the request config", () => {
143
+ const step: MonitorStep = new MonitorStep()
144
+ .setRequestType(HTTPMethod.POST)
145
+ .setRequestHeaders({ "x-test": "1" })
146
+ .setRequestBody('{"a":1}');
147
+ expect(step.data?.requestType).toBe(HTTPMethod.POST);
148
+ expect(step.data?.requestHeaders).toEqual({ "x-test": "1" });
149
+ expect(step.data?.requestBody).toBe('{"a":1}');
150
+ });
151
+ });
152
+
153
+ describe("toJSON / fromJSON / clone", () => {
154
+ test("serializes with the MonitorStep type wrapper", () => {
155
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep(DEFAULT_ARG);
156
+ expect(step.toJSON()["_type"]).toBe(ObjectType.MonitorStep);
157
+ });
158
+
159
+ test("round-trips the step id", () => {
160
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep(DEFAULT_ARG);
161
+ const originalId: string | undefined = step.data?.id;
162
+ const restored: MonitorStep = MonitorStep.fromJSON(step.toJSON());
163
+ expect(restored.data?.id).toBe(originalId);
164
+ });
165
+
166
+ test("clone is an independent copy", () => {
167
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep(DEFAULT_ARG);
168
+ const cloned: MonitorStep = MonitorStep.clone(step);
169
+ expect(cloned).not.toBe(step);
170
+ expect(cloned.data?.id).toBe(step.data?.id);
171
+
172
+ cloned.setRequestType(HTTPMethod.DELETE);
173
+ // Mutating the clone must not touch the original.
174
+ expect(step.data?.requestType).not.toBe(HTTPMethod.DELETE);
175
+ });
176
+
177
+ test("toString produces JSON that parses back to a MonitorStep wrapper", () => {
178
+ const step: MonitorStep = MonitorStep.getDefaultMonitorStep(DEFAULT_ARG);
179
+ const parsed: JSONObject = JSON.parse(step.toString()) as JSONObject;
180
+ expect(parsed["_type"]).toBe(ObjectType.MonitorStep);
181
+ });
182
+ });
183
+ });