Compare commits

...

9 commits

Author SHA1 Message Date
Eric Duminil
01e849389d
Merge a9e03ce784 into e9935d7b3b 2025-01-26 11:56:40 +01:00
Frank Elsinga
a9e03ce784
Merge branch 'master' into fix_mqtt_wildcard_topic 2025-01-26 11:56:24 +01:00
DayShift
7a9191761d
fix: make sure that stripping backslashes for notification urls cannot cause catastophic backtracking (ReDOS) (#5573)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-01-26 11:52:12 +01:00
Moqavem
e9935d7b3b
Add Bale notification provider (#5384) 2024-12-31 22:59:48 +08:00
Eric Duminil
275ab89e62 Add MQTT tests with invalid topics 2024-12-02 08:11:55 +01:00
Eric Duminil
ffcdf29a0d Add MQTT tests with different topics 2024-12-01 22:03:45 +01:00
Eric Duminil
ae439c2522 Allow different topics for monitoring and publishing in MQTT tests 2024-12-01 21:29:30 +01:00
Eric Duminil
a8b8a21f4c Write MQTT published topic in message
So that when wildcards for monitoring are used,
the full published topic is displayed.
2024-12-01 21:27:57 +01:00
Eric Duminil
558195ae6a Allow MQTT topic to have wildcards
This should fix https://github.com/louislam/uptime-kuma/issues/1669
2024-11-29 22:39:57 +01:00
10 changed files with 285 additions and 98 deletions

View file

@ -10,7 +10,7 @@ class MqttMonitorType extends MonitorType {
* @inheritdoc * @inheritdoc
*/ */
async check(monitor, heartbeat, server) { async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, { const [ messageTopic, receivedMessage ] = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
port: monitor.port, port: monitor.port,
username: monitor.mqttUsername, username: monitor.mqttUsername,
password: monitor.mqttPassword, password: monitor.mqttPassword,
@ -24,7 +24,7 @@ class MqttMonitorType extends MonitorType {
if (monitor.mqttCheckType === "keyword") { if (monitor.mqttCheckType === "keyword") {
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) { if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`; heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
heartbeat.status = UP; heartbeat.status = UP;
} else { } else {
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`); throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
@ -101,11 +101,9 @@ class MqttMonitorType extends MonitorType {
}); });
client.on("message", (messageTopic, message) => { client.on("message", (messageTopic, message) => {
if (messageTopic === topic) { client.end();
client.end(); clearTimeout(timeoutID);
clearTimeout(timeoutID); resolve([ messageTopic, message.toString("utf8") ]);
resolve(message.toString("utf8"));
}
}); });
}); });

View file

@ -0,0 +1,34 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Bale extends NotificationProvider {
name = "bale";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://tapi.bale.ai";
try {
await axios.post(
`${url}/bot${notification.baleBotToken}/sendMessage`,
{
chat_id: notification.baleChatID,
text: msg
},
{
headers: {
"content-type": "application/json",
},
}
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Bale;

View file

@ -11,7 +11,8 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully."; const okMsg = "Sent Successfully.";
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com"; const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
const url = `${serverUrl.trim().replace(/\/*$/, "")}/message/push`; // capture group below is nessesary to prevent an ReDOS-attack
const url = `${serverUrl.trim().replace(/([^/])\/+$/, "$1")}/message/push`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null; let valid = msg != null && monitorJSON != null && heartbeatJSON != null;

View file

@ -24,7 +24,7 @@ class Whapi extends NotificationProvider {
"body": msg, "body": msg,
}; };
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/\/+$/, "") + "/messages/text"; let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/([^/])\/+$/, "$1") + "/messages/text";
await axios.post(url, data, config); await axios.post(url, data, config);

View file

@ -4,6 +4,7 @@ const Alerta = require("./notification-providers/alerta");
const AlertNow = require("./notification-providers/alertnow"); const AlertNow = require("./notification-providers/alertnow");
const AliyunSms = require("./notification-providers/aliyun-sms"); const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise"); const Apprise = require("./notification-providers/apprise");
const Bale = require("./notification-providers/bale");
const Bark = require("./notification-providers/bark"); const Bark = require("./notification-providers/bark");
const Bitrix24 = require("./notification-providers/bitrix24"); const Bitrix24 = require("./notification-providers/bitrix24");
const ClickSendSMS = require("./notification-providers/clicksendsms"); const ClickSendSMS = require("./notification-providers/clicksendsms");
@ -71,7 +72,6 @@ const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid"); const SendGrid = require("./notification-providers/send-grid");
class Notification { class Notification {
providerList = {}; providerList = {};
/** /**
@ -90,6 +90,7 @@ class Notification {
new AlertNow(), new AlertNow(),
new AliyunSms(), new AliyunSms(),
new Apprise(), new Apprise(),
new Bale(),
new Bark(), new Bark(),
new Bitrix24(), new Bitrix24(),
new ClickSendSMS(), new ClickSendSMS(),
@ -154,10 +155,10 @@ class Notification {
new GtxMessaging(), new GtxMessaging(),
new Cellsynt(), new Cellsynt(),
new Wpush(), new Wpush(),
new SendGrid() new SendGrid(),
]; ];
for (let item of list) { for (let item of list) {
if (! item.name) { if (!item.name) {
throw new Error("Notification provider without name"); throw new Error("Notification provider without name");
} }
@ -177,9 +178,19 @@ class Notification {
* @returns {Promise<string>} Successful msg * @returns {Promise<string>} Successful msg
* @throws Error with fail msg * @throws Error with fail msg
*/ */
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { static async send(
notification,
msg,
monitorJSON = null,
heartbeatJSON = null
) {
if (this.providerList[notification.type]) { if (this.providerList[notification.type]) {
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON); return this.providerList[notification.type].send(
notification,
msg,
monitorJSON,
heartbeatJSON
);
} else { } else {
throw new Error("Notification type is not supported"); throw new Error("Notification type is not supported");
} }
@ -201,10 +212,9 @@ class Notification {
userID, userID,
]); ]);
if (! bean) { if (!bean) {
throw new Error("notification not found"); throw new Error("notification not found");
} }
} else { } else {
bean = R.dispense("notification"); bean = R.dispense("notification");
} }
@ -234,7 +244,7 @@ class Notification {
userID, userID,
]); ]);
if (! bean) { if (!bean) {
throw new Error("notification not found"); throw new Error("notification not found");
} }
@ -250,7 +260,6 @@ class Notification {
let exists = commandExistsSync("apprise"); let exists = commandExistsSync("apprise");
return exists; return exists;
} }
} }
/** /**
@ -261,16 +270,17 @@ class Notification {
*/ */
async function applyNotificationEveryMonitor(notificationID, userID) { async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [ let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
userID userID,
]); ]);
for (let i = 0; i < monitors.length; i++) { for (let i = 0; i < monitors.length; i++) {
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [ let checkNotification = await R.findOne(
monitors[i].id, "monitor_notification",
notificationID, " monitor_id = ? AND notification_id = ? ",
]); [ monitors[i].id, notificationID ]
);
if (! checkNotification) { if (!checkNotification) {
let relation = R.dispense("monitor_notification"); let relation = R.dispense("monitor_notification");
relation.monitor_id = monitors[i].id; relation.monitor_id = monitors[i].id;
relation.notification_id = notificationID; relation.notification_id = notificationID;

View file

@ -113,6 +113,7 @@ export default {
"alerta": "Alerta", "alerta": "Alerta",
"AlertNow": "AlertNow", "AlertNow": "AlertNow",
"apprise": this.$t("apprise"), "apprise": this.$t("apprise"),
"bale": "Bale",
"Bark": "Bark", "Bark": "Bark",
"Bitrix24": "Bitrix24", "Bitrix24": "Bitrix24",
"clicksendsms": "ClickSend SMS", "clicksendsms": "ClickSend SMS",

View file

@ -0,0 +1,93 @@
<template>
<div class="mb-3">
<label for="bale-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
<HiddenInput id="bale-bot-token" v-model="$parent.notification.baleBotToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetBaleToken" class="form-text">
<a href="https://ble.ir/BotFather" target="_blank">https://ble.ir/BotFather</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="bale-chat-id" class="form-label">{{ $t("Chat ID") }}</label>
<div class="input-group mb-3">
<input id="bale-chat-id" v-model="$parent.notification.baleChatID" type="text" class="form-control" required>
<button v-if="$parent.notification.baleBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetBaleChatID">
{{ $t("Auto Get") }}
</button>
</div>
<div class="form-text">
{{ $t("supportBaleChatID") }}
<p style="margin-top: 8px;">
{{ $t("wayToGetBaleChatID") }}
</p>
<p style="margin-top: 8px;">
<a :href="baleGetUpdatesURL('withToken')" target="_blank" style="word-break: break-word;">{{ baleGetUpdatesURL("masked") }}</a>
</p>
</div>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
import axios from "axios";
export default {
components: {
HiddenInput,
},
methods: {
/**
* Get the URL for bale updates
* @param {string} mode Should the token be masked?
* @returns {string} formatted URL
*/
baleGetUpdatesURL(mode = "masked") {
let token = `<${this.$t("YOUR BOT TOKEN HERE")}>`;
if (this.$parent.notification.baleBotToken) {
if (mode === "withToken") {
token = this.$parent.notification.baleBotToken;
} else if (mode === "masked") {
token = "*".repeat(this.$parent.notification.baleBotToken.length);
}
}
return `https://tapi.bale.ai/bot${token}/getUpdates`;
},
/**
* Get the bale chat ID
* @returns {Promise<void>}
* @throws The chat ID could not be found
*/
async autoGetBaleChatID() {
try {
let res = await axios.get(this.baleGetUpdatesURL("withToken"));
if (res.data.result.length >= 1) {
let update = res.data.result[res.data.result.length - 1];
if (update.channel_post) {
this.$parent.notification.baleChatID = update.channel_post.chat.id;
} else if (update.message) {
this.$parent.notification.baleChatID = update.message.chat.id;
} else {
throw new Error(this.$t("chatIDNotFound"));
}
} else {
throw new Error(this.$t("chatIDNotFound"));
}
} catch (error) {
this.$root.toastError(error.message);
}
},
}
};
</script>

View file

@ -2,6 +2,7 @@ import Alerta from "./Alerta.vue";
import AlertNow from "./AlertNow.vue"; import AlertNow from "./AlertNow.vue";
import AliyunSMS from "./AliyunSms.vue"; import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue"; import Apprise from "./Apprise.vue";
import Bale from "./Bale.vue";
import Bark from "./Bark.vue"; import Bark from "./Bark.vue";
import Bitrix24 from "./Bitrix24.vue"; import Bitrix24 from "./Bitrix24.vue";
import ClickSendSMS from "./ClickSendSMS.vue"; import ClickSendSMS from "./ClickSendSMS.vue";
@ -73,75 +74,76 @@ import SendGrid from "./SendGrid.vue";
* @type { Record<string, any> } * @type { Record<string, any> }
*/ */
const NotificationFormList = { const NotificationFormList = {
"alerta": Alerta, alerta: Alerta,
"AlertNow": AlertNow, AlertNow: AlertNow,
"AliyunSMS": AliyunSMS, AliyunSMS: AliyunSMS,
"apprise": Apprise, apprise: Apprise,
"Bark": Bark, bale: Bale,
"Bitrix24": Bitrix24, Bark: Bark,
"clicksendsms": ClickSendSMS, Bitrix24: Bitrix24,
"CallMeBot": CallMeBot, clicksendsms: ClickSendSMS,
"smsc": SMSC, CallMeBot: CallMeBot,
"DingDing": DingDing, smsc: SMSC,
"discord": Discord, DingDing: DingDing,
"Elks": Elks, discord: Discord,
"Feishu": Feishu, Elks: Elks,
"FreeMobile": FreeMobile, Feishu: Feishu,
"GoogleChat": GoogleChat, FreeMobile: FreeMobile,
"gorush": Gorush, GoogleChat: GoogleChat,
"gotify": Gotify, gorush: Gorush,
"GrafanaOncall": GrafanaOncall, gotify: Gotify,
"HomeAssistant": HomeAssistant, GrafanaOncall: GrafanaOncall,
"HeiiOnCall": HeiiOnCall, HomeAssistant: HomeAssistant,
"Keep": Keep, HeiiOnCall: HeiiOnCall,
"Kook": Kook, Keep: Keep,
"line": Line, Kook: Kook,
"LineNotify": LineNotify, line: Line,
"lunasea": LunaSea, LineNotify: LineNotify,
"matrix": Matrix, lunasea: LunaSea,
"mattermost": Mattermost, matrix: Matrix,
"nostr": Nostr, mattermost: Mattermost,
"ntfy": Ntfy, nostr: Nostr,
"octopush": Octopush, ntfy: Ntfy,
"OneBot": OneBot, octopush: Octopush,
"Onesender": Onesender, OneBot: OneBot,
"Opsgenie": Opsgenie, Onesender: Onesender,
"PagerDuty": PagerDuty, Opsgenie: Opsgenie,
"FlashDuty": FlashDuty, PagerDuty: PagerDuty,
"PagerTree": PagerTree, FlashDuty: FlashDuty,
"promosms": PromoSMS, PagerTree: PagerTree,
"pushbullet": Pushbullet, promosms: PromoSMS,
"PushByTechulus": TechulusPush, pushbullet: Pushbullet,
"PushDeer": PushDeer, PushByTechulus: TechulusPush,
"pushover": Pushover, PushDeer: PushDeer,
"pushy": Pushy, pushover: Pushover,
pushy: Pushy,
"rocket.chat": RocketChat, "rocket.chat": RocketChat,
"serwersms": SerwerSMS, serwersms: SerwerSMS,
"signal": Signal, signal: Signal,
"SIGNL4": SIGNL4, SIGNL4: SIGNL4,
"SMSManager": SMSManager, SMSManager: SMSManager,
"SMSPartner": SMSPartner, SMSPartner: SMSPartner,
"slack": Slack, slack: Slack,
"squadcast": Squadcast, squadcast: Squadcast,
"SMSEagle": SMSEagle, SMSEagle: SMSEagle,
"smtp": STMP, smtp: STMP,
"stackfield": Stackfield, stackfield: Stackfield,
"teams": Teams, teams: Teams,
"telegram": Telegram, telegram: Telegram,
"threema": Threema, threema: Threema,
"twilio": Twilio, twilio: Twilio,
"Splunk": Splunk, Splunk: Splunk,
"webhook": Webhook, webhook: Webhook,
"WeCom": WeCom, WeCom: WeCom,
"GoAlert": GoAlert, GoAlert: GoAlert,
"ServerChan": ServerChan, ServerChan: ServerChan,
"ZohoCliq": ZohoCliq, ZohoCliq: ZohoCliq,
"SevenIO": SevenIO, SevenIO: SevenIO,
"whapi": Whapi, whapi: Whapi,
"gtxmessaging": GtxMessaging, gtxmessaging: GtxMessaging,
"Cellsynt": Cellsynt, Cellsynt: Cellsynt,
"WPush": WPush, WPush: WPush,
"SendGrid": SendGrid, SendGrid: SendGrid,
}; };
export default NotificationFormList; export default NotificationFormList;

View file

@ -429,6 +429,9 @@
"trustProxyDescription": "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind a proxy such as Nginx or Apache, you should enable this.", "trustProxyDescription": "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind a proxy such as Nginx or Apache, you should enable this.",
"wayToGetLineNotifyToken": "You can get an access token from {0}", "wayToGetLineNotifyToken": "You can get an access token from {0}",
"Examples": "Examples", "Examples": "Examples",
"supportBaleChatID": "Support Direct Chat / Group / Channel's Chat ID",
"wayToGetBaleChatID": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
"wayToGetBaleToken": "You can get a token from {0}.",
"Home Assistant URL": "Home Assistant URL", "Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "Long-Lived Access Token", "Long-Lived Access Token": "Long-Lived Access Token",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ", "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ",

View file

@ -9,17 +9,19 @@ const { UP, PENDING } = require("../../src/util");
* Runs an MQTT test with the * Runs an MQTT test with the
* @param {string} mqttSuccessMessage the message that the monitor expects * @param {string} mqttSuccessMessage the message that the monitor expects
* @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform * @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
* @param {string} receivedMessage what message is recieved from the mqtt channel * @param {string} receivedMessage what message is received from the mqtt channel
* @param {string} monitorTopic which MQTT topic is monitored (wildcards are allowed)
* @param {string} publishTopic to which MQTT topic the message is sent
* @returns {Promise<Heartbeat>} the heartbeat produced by the check * @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/ */
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) { async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, monitorTopic = "test", publishTopic = "test") {
const hiveMQContainer = await new HiveMQContainer().start(); const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString(); const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType(); const mqttMonitorType = new MqttMonitorType();
const monitor = { const monitor = {
jsonPath: "firstProp", // always return firstProp for the json-query monitor jsonPath: "firstProp", // always return firstProp for the json-query monitor
hostname: connectionString.split(":", 2).join(":"), hostname: connectionString.split(":", 2).join(":"),
mqttTopic: "test", mqttTopic: monitorTopic,
port: connectionString.split(":")[2], port: connectionString.split(":")[2],
mqttUsername: null, mqttUsername: null,
mqttPassword: null, mqttPassword: null,
@ -35,9 +37,9 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString()); const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
testMqttClient.on("connect", () => { testMqttClient.on("connect", () => {
testMqttClient.subscribe("test", (error) => { testMqttClient.subscribe(monitorTopic, (error) => {
if (!error) { if (!error) {
testMqttClient.publish("test", receivedMessage); testMqttClient.publish(publishTopic, receivedMessage);
} }
}); });
}); });
@ -52,7 +54,7 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
} }
describe("MqttMonitorType", { describe("MqttMonitorType", {
concurrency: true, concurrency: 4,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64") skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => { }, () => {
test("valid keywords (type=default)", async () => { test("valid keywords (type=default)", async () => {
@ -61,11 +63,51 @@ describe("MqttMonitorType", {
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
}); });
test("valid nested topic", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/b/c", "a/b/c");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-");
});
test("valid wildcard topic (with #)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/#", "a/b/c");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-");
});
test("valid wildcard topic (with +)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c", "a/b/c");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-");
});
test("invalid topic", async () => {
await assert.rejects(
testMqtt("keyword will not be checked anyway", null, "message", "x/y/z", "a/b/c"),
new Error("Timeout, Message not received"),
);
});
test("invalid wildcard topic (with #)", async () => {
await assert.rejects(
testMqtt("", null, "# should be last character", "#/c", "a/b/c"),
new Error("Timeout, Message not received"),
);
});
test("invalid wildcard topic (with +)", async () => {
await assert.rejects(
testMqtt("", null, "message", "x/+/z", "a/b/c"),
new Error("Timeout, Message not received"),
);
});
test("valid keywords (type=keyword)", async () => { test("valid keywords (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-"); const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
}); });
test("invalid keywords (type=default)", async () => { test("invalid keywords (type=default)", async () => {
await assert.rejects( await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"), testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
@ -79,12 +121,14 @@ describe("MqttMonitorType", {
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"), new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
); );
}); });
test("valid json-query", async () => { test("valid json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp" // works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}"); const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found"); assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
}); });
test("invalid (because query fails) json-query", async () => { test("invalid (because query fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp" // works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects( await assert.rejects(
@ -92,6 +136,7 @@ describe("MqttMonitorType", {
new Error("Message received but value is not equal to expected value, value was: [undefined]"), new Error("Message received but value is not equal to expected value, value was: [undefined]"),
); );
}); });
test("invalid (because successMessage fails) json-query", async () => { test("invalid (because successMessage fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp" // works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects( await assert.rejects(