From 0eca3011817daa26af0a9e4d6a02ce9f64e52f4b Mon Sep 17 00:00:00 2001 From: Joseph Adams <105917501+josephadamsdev@users.noreply.github.com> Date: Thu, 8 Jan 2026 06:22:08 +0100 Subject: [PATCH] fix: noisy domain expiry checks in monitor editor and missing debuggability (#6637) Co-authored-by: Frank Elsinga Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- server/model/domain_expiry.js | 107 ++++++++++---- server/model/monitor.js | 24 ++-- server/server.js | 8 +- server/translatable-error.js | 15 +- server/util-server.js | 2 +- src/lang/en.json | 7 + src/mixins/socket.js | 26 ++-- src/pages/EditMonitor.vue | 43 ++++-- test/backend-test/test-domain.js | 236 +++++++++++++++++++++++++++---- 9 files changed, 373 insertions(+), 95 deletions(-) diff --git a/server/model/domain_expiry.js b/server/model/domain_expiry.js index 50f088872..4c0ee7d46 100644 --- a/server/model/domain_expiry.js +++ b/server/model/domain_expiry.js @@ -5,8 +5,10 @@ const { parse: parseTld } = require("tldts"); const { getDaysRemaining, getDaysBetween, setting, setSetting } = require("../util-server"); const { Notification } = require("../notification"); const { default: NodeFetchCache, MemoryCache } = require("node-fetch-cache"); +const TranslatableError = require("../translatable-error"); const TABLE = "domain_expiry"; +// NOTE: Keep these type filters in sync with `showDomainExpiryNotification` in `src/pages/EditMonitor.vue`. const urlTypes = [ "websocket-upgrade", "http", "keyword", "json-query", "real-browser" ]; const excludeTypes = [ "docker", "group", "push", "manual", "rabbitmq", "redis" ]; @@ -36,6 +38,7 @@ async function getRdapServer(tld) { return urls[0]; } } + log.debug("rdap", `No RDAP server found for TLD ${tld}`); return null; } @@ -126,7 +129,8 @@ class DomainExpiry extends BeanModel { static parseTld = parseTld; /** - * @returns {(object)} parsed domain components + * @typedef {import("tldts-core").IResult} DomainComponents + * @returns {DomainComponents} parsed domain components */ parseName() { return parseTld(this.domain); @@ -141,26 +145,71 @@ class DomainExpiry extends BeanModel { /** * @param {Monitor} monitor Monitor object - * @returns {Promise} Domain expiry bean + * @throws {TranslatableError} Throws an error if the monitor type is unsupported or missing target. + * @returns {Promise<{ domain: string, tld: string }>} Domain expiry support info */ - static async forMonitor(monitor) { - const m = monitor; - if (excludeTypes.includes(m.type) || m.type?.match(/sql$/)) { - return false; + static async checkSupport(monitor) { + if (excludeTypes.includes(monitor.type) || monitor.type?.match(/sql$/)) { + throw new TranslatableError("domain_expiry_unsupported_monitor_type"); } - const tld = parseTld(urlTypes.includes(m.type) ? m.url : m.type === "grpc-keyword" ? m.grpcUrl : m.hostname); + + let target; + if (urlTypes.includes(monitor.type)) { + target = monitor.url; + } else if (monitor.type === "grpc-keyword") { + target = monitor.grpcUrl; + } else { + target = monitor.hostname; + } + + if (typeof target !== "string" || target.length === 0) { + throw new TranslatableError("domain_expiry_unsupported_missing_target"); + } + + const tld = parseTld(target); + + // Avoid logging for incomplete/invalid input while editing monitors. + if (!tld.domain) { + throw new TranslatableError("domain_expiry_unsupported_invalid_domain", { hostname: tld.hostname }); + } + if ( !tld.publicSuffix) { + throw new TranslatableError("domain_expiry_unsupported_public_suffix", { publicSuffix: tld.publicSuffix }); + } + if (tld.isIp) { + throw new TranslatableError("domain_expiry_unsupported_is_ip", { hostname: tld.hostname }); + } + + // No one-letter public suffix exists; treat this as an incomplete/invalid input while typing. + if (tld.publicSuffix.length < 2) { + throw new TranslatableError("domain_expiry_public_suffix_too_short", { publicSuffix: tld.publicSuffix }); + } + const rdap = await getRdapServer(tld.publicSuffix); if (!rdap) { - log.warn("domain_expiry", `Domain expiry unsupported for '.${tld.publicSuffix}' because its RDAP endpoint is not listed in the IANA database.`); - return false; + // Only warn when the monitor actually has domain expiry notifications enabled. + // The edit monitor page calls this method frequently while the user is typing. + if (Boolean(monitor.domainExpiryNotification)) { + log.warn("domain_expiry", `Domain expiry unsupported for '.${tld.publicSuffix}' because its RDAP endpoint is not listed in the IANA database.`); + } + throw new TranslatableError("domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint", { publicSuffix: tld.publicSuffix }); } - const existing = await DomainExpiry.findByName(tld.domain); + + return { + domain: tld.domain, + tld: tld.publicSuffix, + }; + } + + /** + * @param {string} domainName Domain name + * @returns {Promise} Domain expiry bean + */ + static async findByDomainNameOrCreate(domainName) { + const existing = await DomainExpiry.findByName(domainName); if (existing) { return existing; } - if (tld.domain) { - return await DomainExpiry.createByName(tld.domain); - } + return DomainExpiry.createByName(domainName); } /** @@ -178,12 +227,12 @@ class DomainExpiry extends BeanModel { } /** - * @param {(Monitor)} monitor Monitor object - * @returns {Promise} + * @param {string} domainName Monitor object + * @throws {TranslatableError} If the domain is not supported + * @returns {Promise} the expiry date */ - static async checkExpiry(monitor) { - - let bean = await DomainExpiry.forMonitor(monitor); + static async checkExpiry(domainName) { + let bean = await DomainExpiry.findByDomainNameOrCreate(domainName); let expiryDate; if (bean?.lastCheck && getDaysBetween(new Date(bean.lastCheck), new Date()) < 1) { @@ -209,14 +258,12 @@ class DomainExpiry extends BeanModel { } /** - * @param {Monitor} monitor Monitor instance + * @param {string} domainName the domain name to send notifications for * @param {LooseObject[]} notificationList notification List * @returns {Promise} */ - static async sendNotifications(monitor, notificationList) { - const domain = await DomainExpiry.forMonitor(monitor); - const name = domain.domain; - + static async sendNotifications(domainName, notificationList) { + const domain = await DomainExpiry.findByDomainNameOrCreate(domainName); if (!notificationList.length > 0) { // fail fast. If no notification is set, all the following checks can be skipped. log.debug("domain_expiry", "No notification, no need to send domain notification"); @@ -224,13 +271,13 @@ class DomainExpiry extends BeanModel { } // sanity check if expiry date is valid before calculating days remaining. Should not happen and likely indicates a bug in the code. if (!domain.expiry || isNaN(new Date(domain.expiry).getTime())) { - log.warn("domain_expiry", `No valid expiry date passed to sendNotifications for ${name} (expiry: ${domain.expiry}), skipping notification`); + log.warn("domain_expiry", `No valid expiry date passed to sendNotifications for ${domainName} (expiry: ${domain.expiry}), skipping notification`); return; } const daysRemaining = getDaysRemaining(new Date(), domain.expiry); const lastSent = domain.lastExpiryNotificationSent; - log.debug("domain_expiry", `${name} expires in ${daysRemaining} days`); + log.debug("domain_expiry", `${domainName} expires in ${daysRemaining} days`); let notifyDays = await setting("domainExpiryNotifyDays"); if (notifyDays == null || !Array.isArray(notifyDays)) { @@ -244,19 +291,19 @@ class DomainExpiry extends BeanModel { for (const targetDays of notifyDays) { if (daysRemaining > targetDays) { log.debug( - "domain", - `No need to send domain notification for ${name} (${daysRemaining} days valid) on ${targetDays} deadline.` + "domain_expiry", + `No need to send domain notification for ${domainName} (${daysRemaining} days valid) on ${targetDays} deadline.` ); continue; } else if (lastSent && lastSent <= targetDays) { log.debug( - "domain", - `Notification for ${name} on ${targetDays} deadline sent already, no need to send again.` + "domain_expiry", + `Notification for ${domainName} on ${targetDays} deadline sent already, no need to send again.` ); continue; } const sent = await sendDomainNotificationByTargetDays( - name, + domainName, daysRemaining, targetDays, notificationList diff --git a/server/model/monitor.js b/server/model/monitor.js index 7426e260a..7ed86ca61 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -920,14 +920,15 @@ class Monitor extends BeanModel { if (bean.status !== MAINTENANCE && Boolean(this.domainExpiryNotification)) { try { - const domainExpiryDate = await DomainExpiry.checkExpiry(this); + const supportInfo = await DomainExpiry.checkSupport(this); + const domainExpiryDate = await DomainExpiry.checkExpiry(supportInfo.domain); if (domainExpiryDate) { - DomainExpiry.sendNotifications(this, await Monitor.getNotificationList(this) || []); + DomainExpiry.sendNotifications(supportInfo.domain, await Monitor.getNotificationList(this) || []); } else { - log.debug("monitor", `Failed getting expiration date for domain ${this.name}`); + log.debug("monitor", `Failed getting expiration date for domain ${supportInfo.domain}`); } } catch (error) { - log.warn("monitor", `Failed to get domain expiry for ${this.name} : ${error.message}`); + // purposely not logged due to noise. Is accessible via checkMointor } } @@ -1232,10 +1233,13 @@ class Monitor extends BeanModel { static async sendDomainInfo(io, monitorID, userID) { const monitor = await R.findOne("monitor", "id = ?", [ monitorID ]); - const domain = await DomainExpiry.forMonitor(monitor); - if (domain?.expiry) { - io.to(userID).emit("domainInfo", monitorID, domain.daysRemaining, new Date(domain.expiry)); - } + try { + const supportInfo = await DomainExpiry.checkSupport(monitor); + const domain = await DomainExpiry.findByDomainNameOrCreate(supportInfo.domain); + if (domain?.expiry) { + io.to(userID).emit("domainInfo", monitorID, domain.daysRemaining, new Date(domain.expiry)); + } + } catch (e) {} } /** @@ -1306,7 +1310,7 @@ class Monitor extends BeanModel { * @param {boolean} isFirstBeat Is this beat the first of this monitor? * @param {Monitor} monitor The monitor to send a notification about * @param {Bean} bean Status information about monitor - * @returns {void} + * @returns {Promise} */ static async sendNotification(isFirstBeat, monitor, bean) { if (!isFirstBeat || bean.status === DOWN) { @@ -1363,7 +1367,7 @@ class Monitor extends BeanModel { /** * checks certificate chain for expiring certificates * @param {object} tlsInfoObject Information about certificate - * @returns {void} + * @returns {Promise} */ async checkCertExpiryNotifications(tlsInfoObject) { if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { diff --git a/server/server.js b/server/server.js index caff352a2..7b170dadf 100644 --- a/server/server.js +++ b/server/server.js @@ -698,7 +698,7 @@ let needSetup = false; callback({ ok: false, msg: e.message, - msgi18n: !!e.msgi18n, + msgi18n: !!e.msgi18n }); } }); @@ -990,14 +990,18 @@ let needSetup = false; try { checkLogin(socket); const DomainExpiry = require("./model/domain_expiry"); + const supportInfo = await DomainExpiry.checkSupport(partial); callback({ ok: true, - domain: (await DomainExpiry.forMonitor(partial))?.domain || null + domain: supportInfo.domain, + tld: supportInfo.tld }); } catch (e) { callback({ ok: false, msg: e.message, + msgi18n: !!e.msgi18n, + meta: e.meta ?? {} }); } }); diff --git a/server/translatable-error.js b/server/translatable-error.js index 52528d37d..dbd464137 100644 --- a/server/translatable-error.js +++ b/server/translatable-error.js @@ -1,16 +1,21 @@ +/** + * Error whose message is a translation key. + * @augments Error + */ class TranslatableError extends Error { /** - * Error whose message is a translation key. - * @augments Error + * Indicates that the error message is a translation key. */ + msgi18n = true; + /** * Create a TranslatableError. * @param {string} key - Translation key present in src/lang/en.json + * @param {object} meta Arbitrary metadata */ - constructor(key) { + constructor(key, meta = {}) { super(key); - this.msgi18n = true; - this.key = key; + this.meta = meta; Error.captureStackTrace(this, this.constructor); } } diff --git a/server/util-server.js b/server/util-server.js index f87965da1..7587b0a84 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -487,7 +487,7 @@ const parseCertificateInfo = function (info) { /** * Check if certificate is valid * @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected - * @returns {object} Object containing certificate information + * @returns {null | {valid: boolean, certInfo: object}} Object containing certificate information */ exports.checkCertificate = function (socket) { let certInfoStartTime = dayjs().valueOf(); diff --git a/src/lang/en.json b/src/lang/en.json index a9a3bdc82..b85718559 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1261,6 +1261,13 @@ "labelDomainExpiry": "Domain Exp.", "labelDomainNameExpiryNotification": "Domain Name Expiry Notification", "domainExpiryDescription": "Trigger notification when domain names expires in:", + "domain_expiry_unsupported_monitor_type": "Domain expiry monitoring is not supported for this monitor type", + "domain_expiry_unsupported_missing_target": "No valid domain or hostname is configured for this monitor", + "domain_expiry_unsupported_invalid_domain": "The configured value \"{hostname}\" is not a valid domain name", + "domain_expiry_public_suffix_too_short": "\".{publicSuffix}\" is too short for a top level domain", + "domain_expiry_unsupported_public_suffix": "The domain \"{domain}\" does not have a valid public suffix", + "domain_expiry_unsupported_is_ip": "\"{hostname}\" is an IP address. Domain expiry monitoring requires a domain name", + "domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint": "Domain expiry monitoring is not available for \".{publicSuffix}\" because no RDAP service is listed by IANA", "minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.", "lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors.", "imageResetConfirmation": "Image reset to default", diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 476e82798..308aa6e5b 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -346,25 +346,33 @@ export default { return socket; }, + /** + * Apply translation to a message if possible + * @param {string | {key: string, values: object}} msg Message to translate + * @returns {string} Translated message + */ + applyTranslation(msg) { + if (msg != null && typeof msg === "object") { + return this.$t(msg.key, msg.values); + } else { + return this.$t(msg); + } + }, + /** * Show success or error toast dependent on response status code - * @param {object} res Response object + * @param {{ok:boolean, msg: string, msgi18n: false} | {ok:boolean, msg: string|{key: string, values: object}, msgi18n: true}} res Response object * @returns {void} */ toastRes(res) { - let msg = res.msg; if (res.msgi18n) { - if (msg != null && typeof msg === "object") { - msg = this.$t(msg.key, msg.values); - } else { - msg = this.$t(msg); - } + res.msg = this.applyTranslation(res.msg); } if (res.ok) { - toast.success(msg); + toast.success(res.msg); } else { - toast.error(msg); + toast.error(res.msg); } }, diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index c9f765cc0..ed234fac8 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -817,12 +817,13 @@ -
- +
+ -
+
+ {{ domainExpiryUnsupportedReason }}
@@ -1442,6 +1443,8 @@ export default { // Do not add default value here, please check init() method }, hasDomain: false, + domainExpiryUnsupportedReason: null, + checkMonitorDebounce: null, acceptedStatusCodeOptions: [], acceptedWebsocketCodeOptions: [], dnsresolvetypeOptions: [], @@ -1517,6 +1520,18 @@ export default { }; }, + showDomainExpiryNotification() { + // NOTE: Keep this list in sync with `excludeTypes` in `server/model/domain_expiry.js`. + const excludedTypes = [ "docker", "group", "push", "manual", "rabbitmq", "redis" ]; + const type = this.monitor.type; + + if (!type) { + return false; + } + + return !excludedTypes.includes(type) && !type.match(/sql$/); + }, + pageName() { let name = "Add New Monitor"; if (this.isClone) { @@ -1795,12 +1810,22 @@ message HealthCheckResponse { }, "monitorTypeUrlHost"(data) { - this.$root.getSocket().emit("checkMointor", data, (res) => { - this.hasDomain = !!res?.domain; - if (!res?.domain) { - this.monitor.domainExpiryNotification = false; - } - }); + if (this.checkMonitorDebounce != null) { + clearTimeout(this.checkMonitorDebounce); + } + + if (!this.showDomainExpiryNotification) { + this.hasDomain = false; + this.domainExpiryUnsupportedReason = null; + return; + } + + this.checkMonitorDebounce = setTimeout(() => { + this.$root.getSocket().emit("checkMointor", data, (res) => { + this.hasDomain = !!res?.ok; + this.domainExpiryUnsupportedReason = res.msgi18n ? this.$t(res.msg, res.meta) : res.msg; + }); + }, 500); }, "monitor.type"(newType, oldType) { diff --git a/test/backend-test/test-domain.js b/test/backend-test/test-domain.js index fd188c0f5..def02cc9e 100644 --- a/test/backend-test/test-domain.js +++ b/test/backend-test/test-domain.js @@ -1,6 +1,6 @@ process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(","); -const { describe, test, mock } = require("node:test"); +const { describe, test, mock, before, after } = require("node:test"); const assert = require("node:assert"); const DomainExpiry = require("../../server/model/domain_expiry"); const mockWebhook = require("./notification-providers/mock-webhook"); @@ -19,22 +19,217 @@ describe("Domain Expiry", () => { domainExpiryNotification: true }; - test("getExpiryDate() returns correct expiry date for .wiki domain with no A record", async () => { + before(async () => { await testDb.create(); Notification.init(); + }); + after(async () => { + Settings.stopCacheCleaner(); + await testDb.destroy(); + }); + + test("getExpiryDate() returns correct expiry date for .wiki domain with no A record", async () => { const d = DomainExpiry.createByName("google.wiki"); assert.deepEqual(await d.getExpiryDate(), new Date("2026-11-26T23:59:59.000Z")); }); - test("forMonitor() retrieves expiration date for .com domain from RDAP", async () => { - const domain = await DomainExpiry.forMonitor(monHttpCom); + describe("checkSupport()", () => { + test("allows and correctly parses http monitor with valid domain", async () => { + const supportInfo = await DomainExpiry.checkSupport(monHttpCom); + let expected = { + domain: "google.com", + tld: "com" + }; + assert.deepStrictEqual(supportInfo, expected); + }); + + describe("Target Validation", () => { + test("throws error for empty string target", async () => { + const monitor = { + type: "http", + url: "", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_missing_target"); + return true; + } + ); + }); + + test("throws error for undefined target", async () => { + const monitor = { + type: "http", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_missing_target"); + return true; + } + ); + }); + + test("throws error for null target", async () => { + const monitor = { + type: "http", + url: null, + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_missing_target"); + return true; + } + ); + }); + }); + + describe("Domain Parsing", () => { + test("throws error for invalid domain (no domain part)", async () => { + const monitor = { + type: "http", + url: "https://", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain"); + return true; + } + ); + }); + + test("throws error for IPv4 address instead of domain", async () => { + const monitor = { + type: "http", + url: "https://192.168.1.1", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain"); + return true; + } + ); + }); + + test("throws error for IPv6 address", async () => { + const monitor = { + type: "http", + url: "https://[2001:db8::1]", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain"); + return true; + } + ); + }); + + test("throws error for single-letter TLD", async () => { + const monitor = { + type: "http", + url: "https://example.x", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_public_suffix_too_short"); + return true; + } + ); + }); + }); + + describe("Edge Cases & RDAP Support", () => { + test("handles subdomain correctly", async () => { + const monitor = { + type: "http", + url: "https://api.staging.example.com/v1/users", + domainExpiryNotification: true + }; + const supportInfo = await DomainExpiry.checkSupport(monitor); + assert.strictEqual(supportInfo.domain, "example.com"); + assert.strictEqual(supportInfo.tld, "com"); + }); + + test("handles complex subdomain correctly", async () => { + const monitor = { + type: "http", + url: "https://mail.subdomain.example.org", + domainExpiryNotification: true + }; + const supportInfo = await DomainExpiry.checkSupport(monitor); + assert.strictEqual(supportInfo.domain, "example.org"); + assert.strictEqual(supportInfo.tld, "org"); + }); + + test("handles URL with port correctly", async () => { + const monitor = { + type: "http", + url: "https://example.com:8080/api", + domainExpiryNotification: true + }; + const supportInfo = await DomainExpiry.checkSupport(monitor); + assert.strictEqual(supportInfo.domain, "example.com"); + assert.strictEqual(supportInfo.tld, "com"); + }); + + test("handles URL with query parameters correctly", async () => { + const monitor = { + type: "http", + url: "https://example.com/search?q=test&page=1", + domainExpiryNotification: true + }; + const supportInfo = await DomainExpiry.checkSupport(monitor); + assert.strictEqual(supportInfo.domain, "example.com"); + assert.strictEqual(supportInfo.tld, "com"); + }); + + test("throws error for unsupported TLD without RDAP endpoint", async () => { + const monitor = { + type: "http", + url: "https://example.localhost", + domainExpiryNotification: true + }; + await assert.rejects( + async () => await DomainExpiry.checkSupport(monitor), + (error) => { + assert.strictEqual(error.constructor.name, "TranslatableError"); + assert.strictEqual(error.message, "domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint"); + return true; + } + ); + }); + }); + }); + + test("findByDomainNameOrCreate() retrieves expiration date for .com domain from RDAP", async () => { + const domain = await DomainExpiry.findByDomainNameOrCreate("google.com"); const expiryFromRdap = await domain.getExpiryDate(); // from RDAP assert.deepEqual(expiryFromRdap, new Date("2028-09-14T04:00:00.000Z")); }); test("checkExpiry() caches expiration date in database", async () => { - await DomainExpiry.checkExpiry(monHttpCom); // RDAP -> Cache + await DomainExpiry.checkExpiry("google.com"); // RDAP -> Cache const domain = await DomainExpiry.findByName("google.com"); assert(Date.now() - domain.lastCheck < 5 * 1000); }); @@ -60,27 +255,22 @@ describe("Domain Expiry", () => { const manyDays = 3650; setSetting("domainExpiryNotifyDays", [ manyDays ], "general"); const [ , data ] = await Promise.all([ - DomainExpiry.sendNotifications(monHttpCom, [ notif ]), + DomainExpiry.sendNotifications("google.com", [ notif ]), mockWebhook(hook.port, hook.url) ]); assert.match(data.msg, /will expire in/); - - setTimeout(async () => { - Settings.stopCacheCleaner(); - await testDb.destroy(); - }, 200); }); test("sendNotifications() handles domain with null expiry without sending NaN", async () => { // Regression test for bug: "Domain name will expire in NaN days" - // Mock forMonitor to return a bean with null expiry + // Mock findByDomainNameOrCreate to return a bean with null expiry const mockDomain = { domain: "test-null.com", expiry: null, lastExpiryNotificationSent: null }; - mock.method(DomainExpiry, "forMonitor", async () => mockDomain); + mock.method(DomainExpiry, "findByDomainNameOrCreate", async () => mockDomain); try { const hook = { @@ -88,12 +278,6 @@ describe("Domain Expiry", () => { "url": "should-not-be-called-null" }; - const monTest = { - type: "http", - url: "https://test-null.com", - domainExpiryNotification: true - }; - const notif = { name: "TestNullExpiry", config: JSON.stringify({ @@ -107,7 +291,7 @@ describe("Domain Expiry", () => { // Race between sendNotifications and mockWebhook timeout // If webhook is called, we fail. If it times out, we pass. const result = await Promise.race([ - DomainExpiry.sendNotifications(monTest, [ notif ]), + DomainExpiry.sendNotifications("test-null.com", [ notif ]), mockWebhook(hook.port, hook.url, 500).then(() => { throw new Error("Webhook was called but should not have been for null expiry"); }).catch((e) => { @@ -126,26 +310,20 @@ describe("Domain Expiry", () => { test("sendNotifications() handles domain with undefined expiry without sending NaN", async () => { try { - // Mock forMonitor to return a bean with undefined expiry (newly created bean scenario) + // Mock findByDomainNameOrCreate to return a bean with undefined expiry (newly created bean scenario) const mockDomain = { domain: "test-undefined.com", expiry: undefined, lastExpiryNotificationSent: null }; - mock.method(DomainExpiry, "forMonitor", async () => mockDomain); + mock.method(DomainExpiry, "findByDomainNameOrCreate", async () => mockDomain); const hook = { "port": 3013, "url": "should-not-be-called-undefined" }; - const monTest = { - type: "http", - url: "https://test-undefined.com", - domainExpiryNotification: true - }; - const notif = { name: "TestUndefinedExpiry", config: JSON.stringify({ @@ -159,7 +337,7 @@ describe("Domain Expiry", () => { // Race between sendNotifications and mockWebhook timeout // If webhook is called, we fail. If it times out, we pass. const result = await Promise.race([ - DomainExpiry.sendNotifications(monTest, [ notif ]), + DomainExpiry.sendNotifications("test-undefined.com", [ notif ]), mockWebhook(hook.port, hook.url, 500).then(() => { throw new Error("Webhook was called but should not have been for undefined expiry"); }).catch((e) => {