From 3f7d4ef7d68c7f9f41823b3598d713c616bcaab8 Mon Sep 17 00:00:00 2001
From: Jacques ROUSSEL <jacques.roussel@rouaje.com>
Date: Tue, 4 Mar 2025 11:57:04 +0100
Subject: [PATCH 1/3] feat: add `TCP tls` monitor

---
 server/monitor-types/tls.js  | 77 ++++++++++++++++++++++++++++++++++++
 server/uptime-kuma-server.js |  2 +
 src/pages/EditMonitor.vue    |  9 +++--
 3 files changed, 85 insertions(+), 3 deletions(-)
 create mode 100644 server/monitor-types/tls.js

diff --git a/server/monitor-types/tls.js b/server/monitor-types/tls.js
new file mode 100644
index 000000000..ea9b239ed
--- /dev/null
+++ b/server/monitor-types/tls.js
@@ -0,0 +1,77 @@
+const { MonitorType } = require("./monitor-type");
+const { UP, DOWN } = require("../../src/util");
+const { checkCertificate, setting, setSetting } = require("../util-server");
+const tls = require("tls");
+
+class TlsCertificateMonitorType extends MonitorType {
+    name = "tlsCheck";
+
+    /**
+     * @inheritdoc
+     */
+    async check(monitor, heartbeat, server) {
+        const host = monitor.hostname;
+        const port = monitor.port || 443;
+        let notifyDays = await setting("tlsExpiryNotifyDays");
+        if (notifyDays == null || !Array.isArray(notifyDays)) {
+            // Reset Default
+            await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
+            notifyDays = [ 7, 14, 21 ];
+        }
+
+        try {
+            const options = {
+                host,
+                port,
+                servername: host,
+            };
+
+            // Convert TLS connect to a Promise and await it
+            const tlsInfoObject = await new Promise((resolve, reject) => {
+                const socket = tls.connect(options);
+
+                socket.on("secureConnect", () => {
+                    try {
+                        const info = checkCertificate(socket);
+                        socket.end();
+                        resolve(info);
+                    } catch (error) {
+                        socket.end();
+                        reject(error);
+                    }
+                });
+
+                socket.on("error", (error) => {
+                    reject(error);
+                });
+
+                socket.setTimeout(10000, () => {
+                    socket.end();
+                    reject(new Error("Connection timed out"));
+                });
+            });
+
+            const certInfo = tlsInfoObject.certInfo;
+
+            await monitor.updateTlsInfo(tlsInfoObject);
+            const alertDays = notifyDays.filter(targetDays => targetDays >= certInfo.daysRemaining);
+
+            if (alertDays.length === 0) {
+                heartbeat.status = UP;
+                heartbeat.msg = "";
+            } else {
+                const alertDay = Math.min(...alertDays);
+                heartbeat.status = DOWN;
+                heartbeat.msg = `Certificate expires in less thant ${alertDay} days`;
+            }
+        } catch (error) {
+            heartbeat.status = DOWN;
+            heartbeat.msg = `Error checking SSL certificate: ${error.message}`;
+        }
+    }
+}
+
+module.exports = {
+    TlsCertificateMonitorType,
+};
+
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 062f098d7..324a6c0ed 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -116,6 +116,7 @@ class UptimeKumaServer {
         UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
         UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
         UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
+        UptimeKumaServer.monitorTypeList["tls"] = new TlsCertificateMonitorType();
 
         // Allow all CORS origins (polling) in development
         let cors = undefined;
@@ -554,4 +555,5 @@ const { MqttMonitorType } = require("./monitor-types/mqtt");
 const { SNMPMonitorType } = require("./monitor-types/snmp");
 const { MongodbMonitorType } = require("./monitor-types/mongodb");
 const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
+const { TlsCertificateMonitorType } = require("./monitor-types/tls");
 const Monitor = require("./model/monitor");
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index a83f91cab..70cec0325 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -21,6 +21,9 @@
                                         <option value="port">
                                             TCP Port
                                         </option>
+                                        <option value="tls">
+                                            TCP Port (tls check)
+                                        </option>
                                         <option value="ping">
                                             Ping
                                         </option>
@@ -282,7 +285,7 @@
 
                             <!-- Hostname -->
                             <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
-                            <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
+                            <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp' || monitor.type === 'tls'" class="my-3">
                                 <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
                                 <input
                                     id="hostname"
@@ -297,7 +300,7 @@
 
                             <!-- Port -->
                             <!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
-                            <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3">
+                            <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp' || monitor.type === 'tls'" class="my-3">
                                 <label for="port" class="form-label">{{ $t("Port") }}</label>
                                 <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
                             </div>
@@ -612,7 +615,7 @@
 
                             <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
 
-                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
+                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'tls' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
                                 <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
                                 <label class="form-check-label" for="expiry-notification">
                                     {{ $t("Certificate Expiry Notification") }}

From 2e2acbd48af4aec981a7d47c9d52b1e80d26f86d Mon Sep 17 00:00:00 2001
From: Jacques ROUSSEL <jacques.roussel@rouaje.com>
Date: Mon, 17 Mar 2025 15:19:01 +0100
Subject: [PATCH 2/3] feat: add `TCP tls` monitor (new proposition)

---
 server/model/monitor.js      | 40 +++++++++++++++++++
 server/monitor-types/tls.js  | 77 ------------------------------------
 server/uptime-kuma-server.js |  2 -
 src/pages/EditMonitor.vue    |  9 ++---
 4 files changed, 43 insertions(+), 85 deletions(-)
 delete mode 100644 server/monitor-types/tls.js

diff --git a/server/model/monitor.js b/server/model/monitor.js
index 5999d93e7..08f08d868 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -24,6 +24,7 @@ const { CookieJar } = require("tough-cookie");
 const { HttpsCookieAgent } = require("http-cookie-agent/http");
 const https = require("https");
 const http = require("http");
+const tls = require("tls");
 
 const rootCertificates = rootCertificatesFingerprints();
 
@@ -613,6 +614,45 @@ class Monitor extends BeanModel {
 
                 } else if (this.type === "port") {
                     bean.ping = await tcping(this.hostname, this.port);
+                    if (this.isEnabledExpiryNotification()) {
+                        const host = this.hostname;
+                        const port = this.port || 443;
+                        try {
+                            const options = {
+                                host,
+                                port,
+                                servername: host,
+                            };
+
+                            // Convert TLS connect to a Promise and await it
+                            const tlsInfoObject = await new Promise((resolve, reject) => {
+                                const socket = tls.connect(options);
+
+                                socket.on("secureConnect", () => {
+                                    try {
+                                        const info = checkCertificate(socket);
+                                        socket.end();
+                                        resolve(info);
+                                    } catch (error) {
+                                        socket.end();
+                                        reject(error);
+                                    }
+                                });
+
+                                socket.on("error", (error) => {
+                                    reject(error);
+                                });
+
+                                socket.setTimeout(10000, () => {
+                                    socket.end();
+                                    reject(new Error("Connection timed out"));
+                                });
+                            });
+                            await this.handleTlsInfo(tlsInfoObject);
+                        } catch (error) {
+                            console.log("Retrieve certificate failed");
+                        }
+                    }
                     bean.msg = "";
                     bean.status = UP;
 
diff --git a/server/monitor-types/tls.js b/server/monitor-types/tls.js
deleted file mode 100644
index ea9b239ed..000000000
--- a/server/monitor-types/tls.js
+++ /dev/null
@@ -1,77 +0,0 @@
-const { MonitorType } = require("./monitor-type");
-const { UP, DOWN } = require("../../src/util");
-const { checkCertificate, setting, setSetting } = require("../util-server");
-const tls = require("tls");
-
-class TlsCertificateMonitorType extends MonitorType {
-    name = "tlsCheck";
-
-    /**
-     * @inheritdoc
-     */
-    async check(monitor, heartbeat, server) {
-        const host = monitor.hostname;
-        const port = monitor.port || 443;
-        let notifyDays = await setting("tlsExpiryNotifyDays");
-        if (notifyDays == null || !Array.isArray(notifyDays)) {
-            // Reset Default
-            await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
-            notifyDays = [ 7, 14, 21 ];
-        }
-
-        try {
-            const options = {
-                host,
-                port,
-                servername: host,
-            };
-
-            // Convert TLS connect to a Promise and await it
-            const tlsInfoObject = await new Promise((resolve, reject) => {
-                const socket = tls.connect(options);
-
-                socket.on("secureConnect", () => {
-                    try {
-                        const info = checkCertificate(socket);
-                        socket.end();
-                        resolve(info);
-                    } catch (error) {
-                        socket.end();
-                        reject(error);
-                    }
-                });
-
-                socket.on("error", (error) => {
-                    reject(error);
-                });
-
-                socket.setTimeout(10000, () => {
-                    socket.end();
-                    reject(new Error("Connection timed out"));
-                });
-            });
-
-            const certInfo = tlsInfoObject.certInfo;
-
-            await monitor.updateTlsInfo(tlsInfoObject);
-            const alertDays = notifyDays.filter(targetDays => targetDays >= certInfo.daysRemaining);
-
-            if (alertDays.length === 0) {
-                heartbeat.status = UP;
-                heartbeat.msg = "";
-            } else {
-                const alertDay = Math.min(...alertDays);
-                heartbeat.status = DOWN;
-                heartbeat.msg = `Certificate expires in less thant ${alertDay} days`;
-            }
-        } catch (error) {
-            heartbeat.status = DOWN;
-            heartbeat.msg = `Error checking SSL certificate: ${error.message}`;
-        }
-    }
-}
-
-module.exports = {
-    TlsCertificateMonitorType,
-};
-
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 324a6c0ed..062f098d7 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -116,7 +116,6 @@ class UptimeKumaServer {
         UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
         UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
         UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
-        UptimeKumaServer.monitorTypeList["tls"] = new TlsCertificateMonitorType();
 
         // Allow all CORS origins (polling) in development
         let cors = undefined;
@@ -555,5 +554,4 @@ const { MqttMonitorType } = require("./monitor-types/mqtt");
 const { SNMPMonitorType } = require("./monitor-types/snmp");
 const { MongodbMonitorType } = require("./monitor-types/mongodb");
 const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
-const { TlsCertificateMonitorType } = require("./monitor-types/tls");
 const Monitor = require("./model/monitor");
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 70cec0325..a8b481de3 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -21,9 +21,6 @@
                                         <option value="port">
                                             TCP Port
                                         </option>
-                                        <option value="tls">
-                                            TCP Port (tls check)
-                                        </option>
                                         <option value="ping">
                                             Ping
                                         </option>
@@ -285,7 +282,7 @@
 
                             <!-- Hostname -->
                             <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
-                            <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp' || monitor.type === 'tls'" class="my-3">
+                            <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
                                 <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
                                 <input
                                     id="hostname"
@@ -300,7 +297,7 @@
 
                             <!-- Port -->
                             <!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
-                            <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp' || monitor.type === 'tls'" class="my-3">
+                            <div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'snmp'" class="my-3">
                                 <label for="port" class="form-label">{{ $t("Port") }}</label>
                                 <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
                             </div>
@@ -615,7 +612,7 @@
 
                             <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
 
-                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'tls' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
+                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'port' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
                                 <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
                                 <label class="form-check-label" for="expiry-notification">
                                     {{ $t("Certificate Expiry Notification") }}

From 054d2f7aab566fd56ccfc6df9164b6834bf59a47 Mon Sep 17 00:00:00 2001
From: Jacques ROUSSEL <jacques.roussel@rouaje.com>
Date: Tue, 25 Mar 2025 15:41:20 +0100
Subject: [PATCH 3/3] feat: Refacto `TCP tls` monitor

---
 server/model/monitor.js       | 44 ------------------
 server/monitor-types/tcp.js   | 73 +++++++++++++++++++++++++++++
 server/uptime-kuma-server.js  |  2 +
 test/backend-test/test-tcp.js | 87 +++++++++++++++++++++++++++++++++++
 4 files changed, 162 insertions(+), 44 deletions(-)
 create mode 100644 server/monitor-types/tcp.js
 create mode 100644 test/backend-test/test-tcp.js

diff --git a/server/model/monitor.js b/server/model/monitor.js
index 08f08d868..5377f75f9 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -612,50 +612,6 @@ class Monitor extends BeanModel {
 
                     }
 
-                } else if (this.type === "port") {
-                    bean.ping = await tcping(this.hostname, this.port);
-                    if (this.isEnabledExpiryNotification()) {
-                        const host = this.hostname;
-                        const port = this.port || 443;
-                        try {
-                            const options = {
-                                host,
-                                port,
-                                servername: host,
-                            };
-
-                            // Convert TLS connect to a Promise and await it
-                            const tlsInfoObject = await new Promise((resolve, reject) => {
-                                const socket = tls.connect(options);
-
-                                socket.on("secureConnect", () => {
-                                    try {
-                                        const info = checkCertificate(socket);
-                                        socket.end();
-                                        resolve(info);
-                                    } catch (error) {
-                                        socket.end();
-                                        reject(error);
-                                    }
-                                });
-
-                                socket.on("error", (error) => {
-                                    reject(error);
-                                });
-
-                                socket.setTimeout(10000, () => {
-                                    socket.end();
-                                    reject(new Error("Connection timed out"));
-                                });
-                            });
-                            await this.handleTlsInfo(tlsInfoObject);
-                        } catch (error) {
-                            console.log("Retrieve certificate failed");
-                        }
-                    }
-                    bean.msg = "";
-                    bean.status = UP;
-
                 } else if (this.type === "ping") {
                     bean.ping = await ping(this.hostname, this.packetSize);
                     bean.msg = "";
diff --git a/server/monitor-types/tcp.js b/server/monitor-types/tcp.js
new file mode 100644
index 000000000..815be4db2
--- /dev/null
+++ b/server/monitor-types/tcp.js
@@ -0,0 +1,73 @@
+const { MonitorType } = require("./monitor-type");
+const { UP, DOWN, log, evaluateJsonQuery } = require("../../src/util");
+const { tcping, checkCertificate } = require("../util-server");
+const tls = require("tls");
+
+class TCPMonitorType extends MonitorType {
+    name = "port";
+
+    /**
+     * @inheritdoc
+     */
+    async check(monitor, heartbeat, _server) {
+        try {
+            heartbeat.ping = await tcping(monitor.hostname, monitor.port);
+            heartbeat.msg = "";
+            heartbeat.status = UP;
+        } catch (error) {
+            heartbeat.status = DOWN;
+            heartbeat.msg = "Connection failed";
+            return;
+        }
+
+        if (monitor.isEnabledExpiryNotification()) {
+            let socket = null;
+            try {
+                const options = {
+                    host: monitor.hostname,
+                    port: monitor.port,
+                    servername: monitor.hostname,
+                };
+
+                const tlsInfoObject = await new Promise((resolve, reject) => {
+                    socket = tls.connect(options);
+
+                    socket.on("secureConnect", () => {
+                        try {
+                            const info = checkCertificate(socket);
+                            resolve(info);
+                        } catch (error) {
+                            reject(error);
+                        }
+                    });
+
+                    socket.on("error", (error) => {
+                        reject(error);
+                    });
+
+                    socket.setTimeout(10000, () => {
+                        reject(new Error("Connection timed out"));
+                    });
+                });
+
+                await monitor.handleTlsInfo(tlsInfoObject);
+                if (!tlsInfoObject.valid) {
+                    heartbeat.status = DOWN;
+                    heartbeat.msg = "Certificate is invalid";
+                }
+            } catch (error) {
+                heartbeat.status = DOWN;
+                heartbeat.msg = "Connection failed";
+            } finally {
+                if (socket && !socket.destroyed) {
+                    socket.end();
+                }
+            }
+        }
+    }
+}
+
+module.exports = {
+    TCPMonitorType,
+};
+
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 062f098d7..b0fe4268e 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -116,6 +116,7 @@ class UptimeKumaServer {
         UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
         UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
         UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
+        UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType();
 
         // Allow all CORS origins (polling) in development
         let cors = undefined;
@@ -554,4 +555,5 @@ const { MqttMonitorType } = require("./monitor-types/mqtt");
 const { SNMPMonitorType } = require("./monitor-types/snmp");
 const { MongodbMonitorType } = require("./monitor-types/mongodb");
 const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
+const { TCPMonitorType } = require("./monitor-types/tcp.js");
 const Monitor = require("./model/monitor");
diff --git a/test/backend-test/test-tcp.js b/test/backend-test/test-tcp.js
new file mode 100644
index 000000000..fd5d7dae9
--- /dev/null
+++ b/test/backend-test/test-tcp.js
@@ -0,0 +1,87 @@
+const { describe, test } = require("node:test");
+const assert = require("node:assert");
+const { TCPMonitorType } = require("../../server/monitor-types/tcp");
+const { UP, DOWN, PENDING } = require("../../src/util");
+const net = require("net");
+
+describe("TCP Monitor", () => {
+    async function createTCPServer(port) {
+        return new Promise((resolve, reject) => {
+            const server = net.createServer();
+
+            server.listen(port, () => {
+                resolve(server);
+            });
+
+            server.on("error", (err) => {
+                reject(err);
+            });
+        });
+    }
+
+    test("TCP server is running", async () => {
+        const port = 12345;
+        const server = await createTCPServer(port);
+
+        try {
+            const tcpMonitor = new TCPMonitorType();
+            const monitor = {
+                hostname: "localhost",
+                port: port,
+                isEnabledExpiryNotification: () => false
+            };
+
+            const heartbeat = {
+                msg: "",
+                status: PENDING,
+            };
+
+            await tcpMonitor.check(monitor, heartbeat, {});
+
+            assert.strictEqual(heartbeat.status, UP);
+            assert.strictEqual(heartbeat.msg, "");
+        } finally {
+            server.close();
+        }
+    });
+
+    test("TCP server is not running", async () => {
+        const tcpMonitor = new TCPMonitorType();
+        const monitor = {
+            hostname: "localhost",
+            port: 54321,
+            isEnabledExpiryNotification: () => false
+        };
+
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
+
+        await tcpMonitor.check(monitor, heartbeat, {});
+
+        assert.strictEqual(heartbeat.status, DOWN);
+    });
+
+    test("TCP server with expired or invalid TLS certificate", async (t) => {
+        const tcpMonitor = new TCPMonitorType();
+        const monitor = {
+            hostname: "expired.badssl.com",
+            port: 443,
+            isEnabledExpiryNotification: () => true,
+            handleTlsInfo: async (tlsInfo) => {
+                return tlsInfo;
+            }
+        };
+
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
+
+        await tcpMonitor.check(monitor, heartbeat, {});
+
+        assert.strictEqual(heartbeat.status, DOWN);
+        assert(["Certificate is invalid", "Connection failed"].includes(heartbeat.msg));
+    });
+});