@oneuptime/common 13.0.3 → 13.0.4

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.
@@ -0,0 +1,1109 @@
1
+ import MonitorTemplateUtil from "../../../../Server/Utils/Monitor/MonitorTemplateUtil";
2
+ import DataToProcess from "../../../../Server/Utils/Monitor/DataToProcess";
3
+ import logger from "../../../../Server/Utils/Logger";
4
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
5
+ import { JSONObject } from "../../../../Types/JSON";
6
+ import MonitorType from "../../../../Types/Monitor/MonitorType";
7
+ import ObjectID from "../../../../Types/ObjectID";
8
+ import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
9
+ import IncomingMonitorRequest from "../../../../Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
10
+ import ServerMonitorResponse from "../../../../Types/Monitor/ServerMonitor/ServerMonitorResponse";
11
+ import MetricMonitorResponse from "../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
12
+ import DatabaseMonitorResponse, {
13
+ DatabaseMetricGroupUnavailableReason,
14
+ } from "../../../../Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse";
15
+ import { DatabaseMetricGroup } from "../../../../Types/Monitor/DatabaseMetricCatalog";
16
+ import HTTPMethod from "../../../../Types/API/HTTPMethod";
17
+ import {
18
+ afterEach,
19
+ beforeEach,
20
+ describe,
21
+ expect,
22
+ jest,
23
+ test,
24
+ } from "@jest/globals";
25
+
26
+ /*
27
+ * buildTemplateStorageMap defines the variable surface that incident and
28
+ * alert title/description templates can reference for each monitor type;
29
+ * processTemplateString renders against it. The NetworkDevice branch, the
30
+ * series-label fold and the series-context variables have their own suites
31
+ * (MonitorTemplateUtilNetworkDevice / MonitorTemplatePrototypePollution /
32
+ * MonitorTemplateUtilSeriesContext); this suite pins every other branch.
33
+ */
34
+
35
+ const SERIES_CONTEXT_KEYS: Array<string> = [
36
+ "seriesResourceSuffix",
37
+ "seriesResourceSummary",
38
+ "seriesResourceBlock",
39
+ "seriesDebugCommands",
40
+ ];
41
+
42
+ function probeResponse(
43
+ overrides: Partial<ProbeMonitorResponse> = {},
44
+ ): ProbeMonitorResponse {
45
+ return {
46
+ projectId: ObjectID.generate(),
47
+ monitorStepId: ObjectID.generate(),
48
+ monitorId: ObjectID.generate(),
49
+ probeId: ObjectID.generate(),
50
+ failureCause: "",
51
+ monitoredAt: new Date("2026-09-13T00:00:00.000Z"),
52
+ ...overrides,
53
+ };
54
+ }
55
+
56
+ function build(
57
+ monitorType: MonitorType,
58
+ dataToProcess: DataToProcess,
59
+ extra: { monitor?: Monitor; seriesLabels?: JSONObject } = {},
60
+ ): JSONObject {
61
+ return MonitorTemplateUtil.buildTemplateStorageMap({
62
+ monitorType,
63
+ dataToProcess,
64
+ ...extra,
65
+ });
66
+ }
67
+
68
+ /** The storage map minus the always-present series-context variables. */
69
+ function withoutSeriesContext(map: JSONObject): JSONObject {
70
+ const copy: JSONObject = { ...map };
71
+ for (const key of SERIES_CONTEXT_KEYS) {
72
+ delete copy[key];
73
+ }
74
+ return copy;
75
+ }
76
+
77
+ beforeEach(() => {
78
+ jest.spyOn(logger, "error").mockImplementation(() => {});
79
+ jest.spyOn(logger, "debug").mockImplementation(() => {});
80
+ });
81
+
82
+ afterEach(() => {
83
+ jest.restoreAllMocks();
84
+ });
85
+
86
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — API / Website", () => {
87
+ test.each([MonitorType.API, MonitorType.Website])(
88
+ "%s exposes the parsed JSON body, headers, status, timing and online flag",
89
+ (monitorType: MonitorType) => {
90
+ const map: JSONObject = build(
91
+ monitorType,
92
+ probeResponse({
93
+ responseBody: '{"status":"degraded","nested":{"count":3}}',
94
+ responseHeaders: { "content-type": "application/json" },
95
+ responseCode: 503,
96
+ responseTimeInMs: 812,
97
+ isOnline: false,
98
+ }),
99
+ );
100
+
101
+ expect(withoutSeriesContext(map)).toEqual({
102
+ responseBody: { status: "degraded", nested: { count: 3 } },
103
+ responseHeaders: { "content-type": "application/json" },
104
+ responseStatusCode: 503,
105
+ responseTimeInMs: 812,
106
+ isOnline: false,
107
+ });
108
+ },
109
+ );
110
+
111
+ test("a missing body becomes an empty object", () => {
112
+ const map: JSONObject = build(MonitorType.API, probeResponse());
113
+ expect(map["responseBody"]).toEqual({});
114
+ });
115
+
116
+ test("an empty-string body becomes an empty object", () => {
117
+ const map: JSONObject = build(
118
+ MonitorType.API,
119
+ probeResponse({ responseBody: "" }),
120
+ );
121
+ expect(map["responseBody"]).toEqual({});
122
+ });
123
+
124
+ test("a JSON empty-string literal body is normalised to an empty object", () => {
125
+ const map: JSONObject = build(
126
+ MonitorType.API,
127
+ probeResponse({ responseBody: '""' }),
128
+ );
129
+ expect(map["responseBody"]).toEqual({});
130
+ });
131
+
132
+ test("a non-JSON body (e.g. HTML) is kept verbatim as a string", () => {
133
+ const map: JSONObject = build(
134
+ MonitorType.Website,
135
+ probeResponse({ responseBody: "<html>down</html>" }),
136
+ );
137
+ expect(map["responseBody"]).toBe("<html>down</html>");
138
+ });
139
+
140
+ test("an already-parsed object body is passed through", () => {
141
+ const body: JSONObject = { ok: true };
142
+ const map: JSONObject = build(
143
+ MonitorType.API,
144
+ probeResponse({ responseBody: body }),
145
+ );
146
+ expect(map["responseBody"]).toEqual({ ok: true });
147
+ });
148
+
149
+ test("a JSON array body is exposed as an array", () => {
150
+ const map: JSONObject = build(
151
+ MonitorType.API,
152
+ probeResponse({ responseBody: '[{"id":1},{"id":2}]' }),
153
+ );
154
+ expect(map["responseBody"]).toEqual([{ id: 1 }, { id: 2 }]);
155
+ });
156
+
157
+ test("a body field can be rendered in a title", () => {
158
+ const map: JSONObject = build(
159
+ MonitorType.API,
160
+ probeResponse({
161
+ responseBody: '{"error":{"message":"db timeout"}}',
162
+ responseCode: 500,
163
+ }),
164
+ );
165
+
166
+ expect(
167
+ MonitorTemplateUtil.processTemplateString({
168
+ value: "API {{responseStatusCode}}: {{responseBody.error.message}}",
169
+ storageMap: map,
170
+ }),
171
+ ).toBe("API 500: db timeout");
172
+ });
173
+ });
174
+
175
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — IncomingRequest", () => {
176
+ test("exposes the request body, headers, method and receipt time", () => {
177
+ const receivedAt: Date = new Date("2026-09-13T10:00:00.000Z");
178
+ const request: IncomingMonitorRequest = {
179
+ projectId: ObjectID.generate(),
180
+ monitorId: ObjectID.generate(),
181
+ requestHeaders: { "x-source": "cron" },
182
+ requestBody: { job: "backup" },
183
+ requestMethod: HTTPMethod.POST,
184
+ incomingRequestReceivedAt: receivedAt,
185
+ checkedAt: receivedAt,
186
+ };
187
+
188
+ expect(
189
+ withoutSeriesContext(build(MonitorType.IncomingRequest, request)),
190
+ ).toEqual({
191
+ requestBody: { job: "backup" },
192
+ requestHeaders: { "x-source": "cron" },
193
+ requestMethod: HTTPMethod.POST,
194
+ incomingRequestReceivedAt: receivedAt,
195
+ });
196
+ });
197
+ });
198
+
199
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — Ping / IP / Port", () => {
200
+ test.each([MonitorType.Ping, MonitorType.IP, MonitorType.Port])(
201
+ "%s exposes reachability fields only",
202
+ (monitorType: MonitorType) => {
203
+ const map: JSONObject = build(
204
+ monitorType,
205
+ probeResponse({
206
+ isOnline: false,
207
+ responseTimeInMs: 5000,
208
+ failureCause: "Request timed out",
209
+ isTimeout: true,
210
+ responseCode: 200,
211
+ }),
212
+ );
213
+
214
+ expect(withoutSeriesContext(map)).toEqual({
215
+ isOnline: false,
216
+ responseTimeInMs: 5000,
217
+ failureCause: "Request timed out",
218
+ isTimeout: true,
219
+ });
220
+ },
221
+ );
222
+ });
223
+
224
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — SSLCertificate", () => {
225
+ test("exposes certificate details", () => {
226
+ const createdAt: Date = new Date("2026-01-01T00:00:00.000Z");
227
+ const expiresAt: Date = new Date("2026-10-01T00:00:00.000Z");
228
+
229
+ const map: JSONObject = build(
230
+ MonitorType.SSLCertificate,
231
+ probeResponse({
232
+ isOnline: true,
233
+ failureCause: "",
234
+ sslResponse: {
235
+ isSelfSigned: false,
236
+ createdAt,
237
+ expiresAt,
238
+ commonName: "example.com",
239
+ organizationalUnit: "Ops",
240
+ organization: "Example Inc",
241
+ locality: "London",
242
+ state: "Greater London",
243
+ country: "GB",
244
+ serialNumber: "01AB",
245
+ fingerprint: "AA:BB",
246
+ fingerprint256: "CC:DD",
247
+ issuer: "Not exposed",
248
+ },
249
+ }),
250
+ );
251
+
252
+ expect(withoutSeriesContext(map)).toEqual({
253
+ isOnline: true,
254
+ isSelfSigned: false,
255
+ createdAt,
256
+ expiresAt,
257
+ commonName: "example.com",
258
+ organizationalUnit: "Ops",
259
+ organization: "Example Inc",
260
+ locality: "London",
261
+ state: "Greater London",
262
+ country: "GB",
263
+ serialNumber: "01AB",
264
+ fingerprint: "AA:BB",
265
+ fingerprint256: "CC:DD",
266
+ failureCause: "",
267
+ });
268
+ });
269
+
270
+ test("a missing certificate leaves details undefined without throwing", () => {
271
+ const map: JSONObject = build(
272
+ MonitorType.SSLCertificate,
273
+ probeResponse({ isOnline: false, failureCause: "ECONNREFUSED" }),
274
+ );
275
+
276
+ expect(map["isOnline"]).toBe(false);
277
+ expect(map["failureCause"]).toBe("ECONNREFUSED");
278
+ expect(map["commonName"]).toBeUndefined();
279
+ expect(map["expiresAt"]).toBeUndefined();
280
+ });
281
+ });
282
+
283
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — Server", () => {
284
+ function serverResponse(
285
+ overrides: Partial<ServerMonitorResponse> = {},
286
+ ): ServerMonitorResponse {
287
+ return {
288
+ projectId: ObjectID.generate(),
289
+ monitorId: ObjectID.generate(),
290
+ hostname: "db-01",
291
+ requestReceivedAt: new Date("2026-09-13T09:00:00.000Z"),
292
+ onlyCheckRequestReceivedAt: false,
293
+ failureCause: "",
294
+ ...overrides,
295
+ };
296
+ }
297
+
298
+ test("exposes hostname, receipt time and flattened infrastructure metrics", () => {
299
+ const response: ServerMonitorResponse = serverResponse({
300
+ basicInfrastructureMetrics: {
301
+ cpuMetrics: { percentUsed: 91.5, cores: 8 },
302
+ memoryMetrics: {
303
+ total: 16000,
304
+ free: 1000,
305
+ used: 15000,
306
+ percentUsed: 93.75,
307
+ percentFree: 6.25,
308
+ },
309
+ diskMetrics: [
310
+ {
311
+ diskPath: "/",
312
+ percentUsed: 80,
313
+ percentFree: 20,
314
+ total: 500,
315
+ free: 100,
316
+ used: 400,
317
+ device: "/dev/sda1",
318
+ },
319
+ ],
320
+ },
321
+ processes: [
322
+ {
323
+ pid: 42,
324
+ name: "postgres",
325
+ command: "postgres -D /data",
326
+ cpuPercent: 50,
327
+ },
328
+ ],
329
+ });
330
+
331
+ expect(withoutSeriesContext(build(MonitorType.Server, response))).toEqual({
332
+ hostname: "db-01",
333
+ requestReceivedAt: response.requestReceivedAt,
334
+ failureCause: "",
335
+ cpuUsagePercent: 91.5,
336
+ cpuCores: 8,
337
+ memoryUsagePercent: 93.75,
338
+ memoryFreePercent: 6.25,
339
+ memoryTotalBytes: 16000,
340
+ diskMetrics: [
341
+ { diskPath: "/", usagePercent: 80, freePercent: 20, totalBytes: 500 },
342
+ ],
343
+ processes: [{ pid: 42, name: "postgres", command: "postgres -D /data" }],
344
+ });
345
+ });
346
+
347
+ test("a heartbeat without metrics or processes exposes only the base fields", () => {
348
+ const response: ServerMonitorResponse = serverResponse({
349
+ failureCause: "No heartbeat",
350
+ });
351
+
352
+ expect(withoutSeriesContext(build(MonitorType.Server, response))).toEqual({
353
+ hostname: "db-01",
354
+ requestReceivedAt: response.requestReceivedAt,
355
+ failureCause: "No heartbeat",
356
+ });
357
+ });
358
+
359
+ test("a disk list renders through an each loop", () => {
360
+ const map: JSONObject = build(
361
+ MonitorType.Server,
362
+ serverResponse({
363
+ basicInfrastructureMetrics: {
364
+ cpuMetrics: { percentUsed: 1, cores: 1 },
365
+ memoryMetrics: {
366
+ total: 1,
367
+ free: 1,
368
+ used: 0,
369
+ percentUsed: 0,
370
+ percentFree: 100,
371
+ },
372
+ diskMetrics: [
373
+ {
374
+ diskPath: "/",
375
+ percentUsed: 10,
376
+ percentFree: 90,
377
+ total: 1,
378
+ free: 1,
379
+ used: 0,
380
+ },
381
+ {
382
+ diskPath: "/var",
383
+ percentUsed: 95,
384
+ percentFree: 5,
385
+ total: 1,
386
+ free: 0,
387
+ used: 1,
388
+ },
389
+ ],
390
+ },
391
+ }),
392
+ );
393
+
394
+ expect(
395
+ MonitorTemplateUtil.processTemplateString({
396
+ value: "{{#each diskMetrics}}{{diskPath}}={{usagePercent}}% {{/each}}",
397
+ storageMap: map,
398
+ }),
399
+ ).toBe("/=10% /var=95% ");
400
+ });
401
+ });
402
+
403
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — CustomJavaScriptCode", () => {
404
+ test("exposes the script result, error, logs and timing", () => {
405
+ const map: JSONObject = build(
406
+ MonitorType.CustomJavaScriptCode,
407
+ probeResponse({
408
+ failureCause: "Script threw",
409
+ customCodeMonitorResponse: {
410
+ result: { healthy: false },
411
+ scriptError: "TypeError: x is undefined",
412
+ logMessages: ["starting", "failed"],
413
+ capturedMetrics: [],
414
+ executionTimeInMS: 120,
415
+ },
416
+ }),
417
+ );
418
+
419
+ expect(withoutSeriesContext(map)).toEqual({
420
+ executionTimeInMs: 120,
421
+ result: { healthy: false },
422
+ scriptError: "TypeError: x is undefined",
423
+ logMessages: ["starting", "failed"],
424
+ failureCause: "Script threw",
425
+ });
426
+ });
427
+
428
+ test("logMessages defaults to an empty list when there is no script response", () => {
429
+ const map: JSONObject = build(
430
+ MonitorType.CustomJavaScriptCode,
431
+ probeResponse(),
432
+ );
433
+
434
+ expect(map["logMessages"]).toEqual([]);
435
+ expect(map["result"]).toBeUndefined();
436
+ expect(map["executionTimeInMs"]).toBeUndefined();
437
+ });
438
+ });
439
+
440
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — SyntheticMonitor", () => {
441
+ test("exposes one entry per browser / screen-size run", () => {
442
+ const map: JSONObject = build(
443
+ MonitorType.SyntheticMonitor,
444
+ probeResponse({
445
+ failureCause: "1 of 2 runs failed",
446
+ syntheticMonitorResponse: [
447
+ {
448
+ result: "ok",
449
+ logMessages: ["a"],
450
+ capturedMetrics: [],
451
+ executionTimeInMS: 1000,
452
+ browserType: "Chromium" as any,
453
+ screenSizeType: "Desktop" as any,
454
+ },
455
+ {
456
+ result: undefined,
457
+ scriptError: "Timeout",
458
+ logMessages: undefined as unknown as Array<string>,
459
+ capturedMetrics: [],
460
+ executionTimeInMS: 30000,
461
+ browserType: "Firefox" as any,
462
+ screenSizeType: "Mobile" as any,
463
+ screenshots: { final: "base64" },
464
+ },
465
+ ],
466
+ }),
467
+ );
468
+
469
+ expect(withoutSeriesContext(map)).toEqual({
470
+ syntheticResponses: [
471
+ {
472
+ executionTimeInMs: 1000,
473
+ result: "ok",
474
+ scriptError: undefined,
475
+ logMessages: ["a"],
476
+ screenshots: undefined,
477
+ browserType: "Chromium",
478
+ screenSizeType: "Desktop",
479
+ },
480
+ {
481
+ executionTimeInMs: 30000,
482
+ result: undefined,
483
+ scriptError: "Timeout",
484
+ logMessages: [],
485
+ screenshots: { final: "base64" },
486
+ browserType: "Firefox",
487
+ screenSizeType: "Mobile",
488
+ },
489
+ ],
490
+ failureCause: "1 of 2 runs failed",
491
+ });
492
+ });
493
+
494
+ test("no runs yields an empty syntheticResponses list", () => {
495
+ const map: JSONObject = build(
496
+ MonitorType.SyntheticMonitor,
497
+ probeResponse(),
498
+ );
499
+ expect(map["syntheticResponses"]).toEqual([]);
500
+ });
501
+
502
+ test("an indexed run renders in a template", () => {
503
+ const map: JSONObject = build(
504
+ MonitorType.SyntheticMonitor,
505
+ probeResponse({
506
+ syntheticMonitorResponse: [
507
+ {
508
+ result: undefined,
509
+ scriptError: "selector not found",
510
+ logMessages: [],
511
+ capturedMetrics: [],
512
+ executionTimeInMS: 5,
513
+ browserType: "Webkit" as any,
514
+ screenSizeType: "Tablet" as any,
515
+ },
516
+ ],
517
+ }),
518
+ );
519
+
520
+ expect(
521
+ MonitorTemplateUtil.processTemplateString({
522
+ value:
523
+ "{{syntheticResponses[0].browserType}}: {{syntheticResponses[0].scriptError}}",
524
+ storageMap: map,
525
+ }),
526
+ ).toBe("Webkit: selector not found");
527
+ });
528
+ });
529
+
530
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — DNS", () => {
531
+ test("exposes status fields, records and the flat record value list", () => {
532
+ const map: JSONObject = build(
533
+ MonitorType.DNS,
534
+ probeResponse({
535
+ isOnline: true,
536
+ dnsResponse: {
537
+ isOnline: true,
538
+ responseTimeInMs: 14,
539
+ failureCause: "",
540
+ isTimeout: false,
541
+ isDnssecValid: true,
542
+ records: [
543
+ { type: "A" as any, value: "10.0.0.1", ttl: 300 },
544
+ { type: "A" as any, value: "10.0.0.2" },
545
+ ],
546
+ },
547
+ }),
548
+ );
549
+
550
+ expect(withoutSeriesContext(map)).toEqual({
551
+ isOnline: true,
552
+ responseTimeInMs: 14,
553
+ failureCause: "",
554
+ isTimeout: false,
555
+ isDnssecValid: true,
556
+ records: [
557
+ { type: "A", value: "10.0.0.1", ttl: 300 },
558
+ { type: "A", value: "10.0.0.2", ttl: undefined },
559
+ ],
560
+ recordValues: ["10.0.0.1", "10.0.0.2"],
561
+ });
562
+ });
563
+
564
+ test("no DNS response leaves records off the map", () => {
565
+ const map: JSONObject = build(
566
+ MonitorType.DNS,
567
+ probeResponse({ isOnline: false }),
568
+ );
569
+
570
+ expect(map["isOnline"]).toBe(false);
571
+ expect("records" in map).toBe(false);
572
+ expect("recordValues" in map).toBe(false);
573
+ });
574
+ });
575
+
576
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — Domain", () => {
577
+ test("exposes WHOIS/RDAP registration details", () => {
578
+ const map: JSONObject = build(
579
+ MonitorType.Domain,
580
+ probeResponse({
581
+ isOnline: true,
582
+ domainResponse: {
583
+ isOnline: true,
584
+ responseTimeInMs: 300,
585
+ failureCause: "",
586
+ domainName: "example.com",
587
+ registrar: "Registrar Inc",
588
+ createdDate: "2000-01-01",
589
+ updatedDate: "2025-01-01",
590
+ expiresDate: "2027-01-01",
591
+ nameServers: ["ns1.example.com", "ns2.example.com"],
592
+ domainStatus: ["clientTransferProhibited"],
593
+ dnssec: "unsigned",
594
+ lookupMethod: "RDAP" as any,
595
+ registrarUrl: "not exposed",
596
+ },
597
+ }),
598
+ );
599
+
600
+ expect(withoutSeriesContext(map)).toEqual({
601
+ isOnline: true,
602
+ responseTimeInMs: 300,
603
+ failureCause: "",
604
+ domainName: "example.com",
605
+ registrar: "Registrar Inc",
606
+ createdDate: "2000-01-01",
607
+ updatedDate: "2025-01-01",
608
+ expiresDate: "2027-01-01",
609
+ nameServers: ["ns1.example.com", "ns2.example.com"],
610
+ domainStatus: ["clientTransferProhibited"],
611
+ dnssec: "unsigned",
612
+ lookupMethod: "RDAP",
613
+ });
614
+ });
615
+ });
616
+
617
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — DNSSEC", () => {
618
+ test("exposes chain status and record counts rather than the raw records", () => {
619
+ const map: JSONObject = build(
620
+ MonitorType.DNSSEC,
621
+ probeResponse({
622
+ isOnline: true,
623
+ dnssecResponse: {
624
+ isOnline: true,
625
+ responseTimeInMs: 90,
626
+ failureCause: "",
627
+ domainName: "example.com",
628
+ isZoneSigned: true,
629
+ dnskeys: [
630
+ { flags: 257, algorithm: 13 },
631
+ { flags: 256, algorithm: 13 },
632
+ ],
633
+ parentDsRecords: [
634
+ { keyTag: 1, algorithm: 13, digestType: 2, digest: "ab" },
635
+ ],
636
+ isParentDsPresent: true,
637
+ rrsigs: [],
638
+ earliestSignatureExpiration: "2026-09-20T00:00:00Z",
639
+ daysUntilSignatureExpiry: 7,
640
+ resolverChecks: [],
641
+ resolverConsensusAd: true,
642
+ nameserverChecks: [],
643
+ isNameserverConsistent: false,
644
+ isChainValid: true,
645
+ },
646
+ }),
647
+ );
648
+
649
+ expect(withoutSeriesContext(map)).toEqual({
650
+ isOnline: true,
651
+ responseTimeInMs: 90,
652
+ failureCause: "",
653
+ domainName: "example.com",
654
+ isZoneSigned: true,
655
+ isParentDsPresent: true,
656
+ isChainValid: true,
657
+ resolverConsensusAd: true,
658
+ isNameserverConsistent: false,
659
+ earliestSignatureExpiration: "2026-09-20T00:00:00Z",
660
+ daysUntilSignatureExpiry: 7,
661
+ dnskeyCount: 2,
662
+ dsRecordCount: 1,
663
+ rrsigCount: 0,
664
+ });
665
+ });
666
+
667
+ test("no DNSSEC response leaves counts undefined", () => {
668
+ const map: JSONObject = build(MonitorType.DNSSEC, probeResponse());
669
+ expect(map["dnskeyCount"]).toBeUndefined();
670
+ expect(map["dsRecordCount"]).toBeUndefined();
671
+ expect(map["rrsigCount"]).toBeUndefined();
672
+ });
673
+ });
674
+
675
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — Database", () => {
676
+ function databaseResponse(
677
+ overrides: Partial<DatabaseMonitorResponse> = {},
678
+ ): DatabaseMonitorResponse {
679
+ return {
680
+ isOnline: true,
681
+ responseTimeInMs: 22,
682
+ failureCause: "",
683
+ metrics: {},
684
+ collectedGroups: [],
685
+ unavailableGroups: [],
686
+ connectionError: null,
687
+ ...overrides,
688
+ };
689
+ }
690
+
691
+ test("exposes groups, metrics and a one-line collection issue summary", () => {
692
+ const map: JSONObject = build(
693
+ MonitorType.Database,
694
+ probeResponse({
695
+ isOnline: true,
696
+ databaseMonitorResponse: databaseResponse({
697
+ engineVersion: "PostgreSQL 17.2",
698
+ metrics: { ["DatabaseConnections" as any]: 12 },
699
+ collectedGroups: [DatabaseMetricGroup.Connections],
700
+ unavailableGroups: [
701
+ {
702
+ group: DatabaseMetricGroup.Replication,
703
+ reason: DatabaseMetricGroupUnavailableReason.MissingPermission,
704
+ message: "permission denied for pg_stat_replication",
705
+ remediation: "GRANT pg_monitor TO monitoring_user",
706
+ },
707
+ {
708
+ group: DatabaseMetricGroup.Locks,
709
+ reason: DatabaseMetricGroupUnavailableReason.Timeout,
710
+ message: "query timed out",
711
+ },
712
+ ],
713
+ }),
714
+ }),
715
+ );
716
+
717
+ expect(withoutSeriesContext(map)).toEqual({
718
+ isOnline: true,
719
+ responseTimeInMs: 22,
720
+ failureCause: "",
721
+ connectionError: null,
722
+ engineVersion: "PostgreSQL 17.2",
723
+ collectedGroups: ["Connections"],
724
+ unavailableGroups: [
725
+ {
726
+ group: "Replication",
727
+ reason: "MissingPermission",
728
+ message: "permission denied for pg_stat_replication",
729
+ remediation: "GRANT pg_monitor TO monitoring_user",
730
+ },
731
+ {
732
+ group: "Locks",
733
+ reason: "Timeout",
734
+ message: "query timed out",
735
+ remediation: undefined,
736
+ },
737
+ ],
738
+ collectionIssueSummary:
739
+ "Replication: permission denied for pg_stat_replication; Locks: query timed out",
740
+ metrics: { DatabaseConnections: 12 },
741
+ });
742
+ });
743
+
744
+ test("full collection yields an empty summary string", () => {
745
+ const map: JSONObject = build(
746
+ MonitorType.Database,
747
+ probeResponse({ databaseMonitorResponse: databaseResponse() }),
748
+ );
749
+ expect(map["collectionIssueSummary"]).toBe("");
750
+ expect(map["unavailableGroups"]).toEqual([]);
751
+ });
752
+
753
+ test("no database response still defines the list and map defaults", () => {
754
+ const map: JSONObject = build(
755
+ MonitorType.Database,
756
+ probeResponse({ isOnline: false }),
757
+ );
758
+
759
+ expect(map["isOnline"]).toBe(false);
760
+ expect(map["collectedGroups"]).toEqual([]);
761
+ expect(map["unavailableGroups"]).toEqual([]);
762
+ expect(map["collectionIssueSummary"]).toBe("");
763
+ expect(map["metrics"]).toEqual({});
764
+ });
765
+ });
766
+
767
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — metric-backed types", () => {
768
+ const metricTypes: Array<MonitorType> = [
769
+ MonitorType.Metrics,
770
+ MonitorType.Kubernetes,
771
+ MonitorType.Docker,
772
+ MonitorType.Host,
773
+ MonitorType.Podman,
774
+ MonitorType.DockerSwarm,
775
+ MonitorType.Proxmox,
776
+ MonitorType.VMware,
777
+ MonitorType.Ceph,
778
+ ];
779
+
780
+ function metricResponse(queryConfigs: Array<unknown>): MetricMonitorResponse {
781
+ return {
782
+ projectId: ObjectID.generate(),
783
+ monitorId: ObjectID.generate(),
784
+ metricResult: [],
785
+ metricViewConfig: { queryConfigs, formulaConfigs: [] } as any,
786
+ };
787
+ }
788
+
789
+ test.each(metricTypes)(
790
+ "%s exposes the first query's metric name",
791
+ (monitorType: MonitorType) => {
792
+ const map: JSONObject = build(
793
+ monitorType,
794
+ metricResponse([
795
+ {
796
+ metricQueryData: {
797
+ filterData: { metricName: "k8s.pod.cpu.utilization" },
798
+ },
799
+ },
800
+ { metricQueryData: { filterData: { metricName: "second" } } },
801
+ ]),
802
+ );
803
+
804
+ expect(withoutSeriesContext(map)).toEqual({
805
+ metricName: "k8s.pod.cpu.utilization",
806
+ });
807
+ },
808
+ );
809
+
810
+ test("no query configs yields an empty metric name", () => {
811
+ expect(build(MonitorType.Metrics, metricResponse([]))["metricName"]).toBe(
812
+ "",
813
+ );
814
+ });
815
+
816
+ test("a query without filter data yields an empty metric name", () => {
817
+ expect(
818
+ build(MonitorType.Metrics, metricResponse([{ metricQueryData: {} }]))[
819
+ "metricName"
820
+ ],
821
+ ).toBe("");
822
+ });
823
+
824
+ test("a response without a view config yields an empty metric name", () => {
825
+ expect(
826
+ build(MonitorType.Kubernetes, {} as unknown as DataToProcess)[
827
+ "metricName"
828
+ ],
829
+ ).toBe("");
830
+ });
831
+ });
832
+
833
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — ExternalStatusPage", () => {
834
+ test("exposes overall status, scope and component statuses", () => {
835
+ const map: JSONObject = build(
836
+ MonitorType.ExternalStatusPage,
837
+ probeResponse({
838
+ isOnline: true,
839
+ externalStatusPageResponse: {
840
+ isOnline: true,
841
+ overallStatus: "major_outage",
842
+ activeIncidentCount: 2,
843
+ responseTimeInMs: 150,
844
+ failureCause: "",
845
+ provider: "Atlassian Statuspage",
846
+ componentGroupName: "APIs",
847
+ componentName: "REST API",
848
+ rawBody: "not exposed",
849
+ componentStatuses: [
850
+ {
851
+ name: "REST API",
852
+ status: "major_outage",
853
+ description: "Errors",
854
+ groupName: "APIs",
855
+ },
856
+ { name: "Dashboard", status: "operational" },
857
+ ],
858
+ },
859
+ }),
860
+ );
861
+
862
+ expect(withoutSeriesContext(map)).toEqual({
863
+ isOnline: true,
864
+ responseTimeInMs: 150,
865
+ failureCause: "",
866
+ overallStatus: "major_outage",
867
+ activeIncidentCount: 2,
868
+ provider: "Atlassian Statuspage",
869
+ componentGroup: "APIs",
870
+ componentName: "REST API",
871
+ componentStatuses: [
872
+ {
873
+ name: "REST API",
874
+ status: "major_outage",
875
+ description: "Errors",
876
+ groupName: "APIs",
877
+ },
878
+ {
879
+ name: "Dashboard",
880
+ status: "operational",
881
+ description: undefined,
882
+ groupName: undefined,
883
+ },
884
+ ],
885
+ });
886
+ });
887
+
888
+ test("no response leaves componentStatuses off the map", () => {
889
+ const map: JSONObject = build(
890
+ MonitorType.ExternalStatusPage,
891
+ probeResponse({ isOnline: false }),
892
+ );
893
+ expect(map["isOnline"]).toBe(false);
894
+ expect("componentStatuses" in map).toBe(false);
895
+ });
896
+ });
897
+
898
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — types without a branch", () => {
899
+ test.each([MonitorType.Manual, MonitorType.Logs, MonitorType.Traces])(
900
+ "%s exposes only the series-context variables",
901
+ (monitorType: MonitorType) => {
902
+ const map: JSONObject = build(monitorType, probeResponse());
903
+ expect(Object.keys(map).sort()).toEqual([...SERIES_CONTEXT_KEYS].sort());
904
+ },
905
+ );
906
+ });
907
+
908
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — monitor identity", () => {
909
+ function monitor(overrides: Partial<Monitor> = {}): Monitor {
910
+ const model: Monitor = new Monitor();
911
+ Object.assign(model, overrides);
912
+ return model;
913
+ }
914
+
915
+ test("exposes name, id, description, slug and type", () => {
916
+ const id: ObjectID = ObjectID.generate();
917
+ const map: JSONObject = build(MonitorType.Ping, probeResponse(), {
918
+ monitor: monitor({
919
+ id,
920
+ name: "Edge router",
921
+ description: "Pings the edge",
922
+ slug: "edge-router",
923
+ monitorType: MonitorType.Ping,
924
+ }),
925
+ });
926
+
927
+ expect(map["monitorName"]).toBe("Edge router");
928
+ expect(map["monitorId"]).toBe(id.toString());
929
+ expect(map["monitorDescription"]).toBe("Pings the edge");
930
+ expect(map["monitorSlug"]).toBe("edge-router");
931
+ expect(map["monitorType"]).toBe(MonitorType.Ping);
932
+ });
933
+
934
+ test("identity is exposed even for types without a data branch", () => {
935
+ const map: JSONObject = build(MonitorType.Manual, probeResponse(), {
936
+ monitor: monitor({ name: "Manual one" }),
937
+ });
938
+ expect(map["monitorName"]).toBe("Manual one");
939
+ });
940
+
941
+ test("unset identity fields are not added to the map", () => {
942
+ const map: JSONObject = build(MonitorType.Ping, probeResponse(), {
943
+ monitor: monitor({ name: "Only a name" }),
944
+ });
945
+
946
+ expect(map["monitorName"]).toBe("Only a name");
947
+ for (const key of [
948
+ "monitorId",
949
+ "monitorDescription",
950
+ "monitorSlug",
951
+ "monitorType",
952
+ ]) {
953
+ expect(key in map).toBe(false);
954
+ }
955
+ });
956
+
957
+ test("no monitor means no identity fields", () => {
958
+ const map: JSONObject = build(MonitorType.Ping, probeResponse());
959
+ expect("monitorName" in map).toBe(false);
960
+ expect("monitorId" in map).toBe(false);
961
+ });
962
+
963
+ test("identity renders alongside monitor data in one template", () => {
964
+ const map: JSONObject = build(
965
+ MonitorType.Ping,
966
+ probeResponse({ failureCause: "Host unreachable" }),
967
+ { monitor: monitor({ name: "Edge router" }) },
968
+ );
969
+
970
+ expect(
971
+ MonitorTemplateUtil.processTemplateString({
972
+ value: "{{monitorName}} is down: {{failureCause}}",
973
+ storageMap: map,
974
+ }),
975
+ ).toBe("Edge router is down: Host unreachable");
976
+ });
977
+ });
978
+
979
+ describe("MonitorTemplateUtil.buildTemplateStorageMap — failure containment", () => {
980
+ test("a throwing payload is logged and still returns the series-context variables", () => {
981
+ const hostile: DataToProcess = {} as unknown as DataToProcess;
982
+ Object.defineProperty(hostile, "isOnline", {
983
+ get: (): never => {
984
+ throw new Error("boom");
985
+ },
986
+ });
987
+
988
+ const map: JSONObject = build(MonitorType.Ping, hostile, {
989
+ monitor: (() => {
990
+ const model: Monitor = new Monitor();
991
+ model.name = "Still named";
992
+ return model;
993
+ })(),
994
+ });
995
+
996
+ expect(logger.error).toHaveBeenCalled();
997
+ expect(map["monitorName"]).toBe("Still named");
998
+ for (const key of SERIES_CONTEXT_KEYS) {
999
+ expect(map[key]).toBe("");
1000
+ }
1001
+ expect("isOnline" in map).toBe(false);
1002
+ });
1003
+
1004
+ test("series labels are folded even onto a branch's own data", () => {
1005
+ const map: JSONObject = build(
1006
+ MonitorType.Ping,
1007
+ probeResponse({ isOnline: true }),
1008
+ { seriesLabels: { "host.name": "web-1", region: "eu" } },
1009
+ );
1010
+
1011
+ expect(map["isOnline"]).toBe(true);
1012
+ expect(map["host"]).toEqual({ name: "web-1" });
1013
+ expect(map["region"]).toBe("eu");
1014
+ expect(map["seriesLabels"]).toEqual({ "host.name": "web-1", region: "eu" });
1015
+ });
1016
+
1017
+ test("null and undefined label values are not folded", () => {
1018
+ const map: JSONObject = build(MonitorType.Metrics, {} as DataToProcess, {
1019
+ seriesLabels: { kept: "yes", dropped: null, "a.b": undefined as any },
1020
+ });
1021
+
1022
+ expect(map["kept"]).toBe("yes");
1023
+ expect("dropped" in map).toBe(false);
1024
+ expect("a" in map).toBe(false);
1025
+ });
1026
+
1027
+ test("a dotted label replaces a scalar at the same prefix with a nested object", () => {
1028
+ const map: JSONObject = build(
1029
+ MonitorType.Metrics,
1030
+ {
1031
+ metricViewConfig: {
1032
+ queryConfigs: [
1033
+ { metricQueryData: { filterData: { metricName: "cpu" } } },
1034
+ ],
1035
+ },
1036
+ } as unknown as DataToProcess,
1037
+ { seriesLabels: { "metricName.unit": "%" } },
1038
+ );
1039
+
1040
+ expect(map["metricName"]).toEqual({ unit: "%" });
1041
+ });
1042
+ });
1043
+
1044
+ describe("MonitorTemplateUtil.processTemplateString", () => {
1045
+ test.each([undefined, ""])(
1046
+ "%j renders as an empty string",
1047
+ (value: string | undefined) => {
1048
+ expect(
1049
+ MonitorTemplateUtil.processTemplateString({
1050
+ value,
1051
+ storageMap: { a: 1 },
1052
+ }),
1053
+ ).toBe("");
1054
+ },
1055
+ );
1056
+
1057
+ test("text without placeholders is returned unchanged", () => {
1058
+ expect(
1059
+ MonitorTemplateUtil.processTemplateString({
1060
+ value: "Plain title",
1061
+ storageMap: { a: 1 },
1062
+ }),
1063
+ ).toBe("Plain title");
1064
+ });
1065
+
1066
+ test("replaces top-level and nested placeholders", () => {
1067
+ expect(
1068
+ MonitorTemplateUtil.processTemplateString({
1069
+ value: "{{a}} / {{b.c}}",
1070
+ storageMap: { a: 1, b: { c: "two" } },
1071
+ }),
1072
+ ).toBe("1 / two");
1073
+ });
1074
+
1075
+ test("replaces a repeated placeholder everywhere", () => {
1076
+ expect(
1077
+ MonitorTemplateUtil.processTemplateString({
1078
+ value: "{{x}}-{{x}}-{{x}}",
1079
+ storageMap: { x: "y" },
1080
+ }),
1081
+ ).toBe("y-y-y");
1082
+ });
1083
+
1084
+ test("an unknown placeholder is left in place", () => {
1085
+ expect(
1086
+ MonitorTemplateUtil.processTemplateString({
1087
+ value: "value: {{missing}}",
1088
+ storageMap: {},
1089
+ }),
1090
+ ).toBe("value: {{missing}}");
1091
+ });
1092
+
1093
+ test("a failure inside the renderer returns the original template", () => {
1094
+ const hostileMap: JSONObject = {};
1095
+ Object.defineProperty(hostileMap, "boom", {
1096
+ enumerable: true,
1097
+ get: (): never => {
1098
+ throw new Error("renderer blew up");
1099
+ },
1100
+ });
1101
+
1102
+ expect(
1103
+ MonitorTemplateUtil.processTemplateString({
1104
+ value: "Title {{boom}}",
1105
+ storageMap: hostileMap,
1106
+ }),
1107
+ ).toBe("Title {{boom}}");
1108
+ });
1109
+ });