fix: preserve Domain Name Expiry Notification setting when editing monitor (#6994)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
This commit is contained in:
Rohit Darekar
2026-03-01 05:08:29 +05:30
committed by GitHub
parent 075aa61806
commit ce740724d8
6 changed files with 61 additions and 163 deletions

View File

@@ -220,17 +220,11 @@ class DomainExpiry extends BeanModel {
const tld = parseTld(target);
// Avoid logging for incomplete/invalid input while editing monitors.
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 });
}
// It must be checked first, filter out non-ICANN domains.
if (!tld.isIcann) {
throw new TranslatableError("domain_expiry_unsupported_is_icann", {
domain: tld.domain,
// If domain is null, use hostname as fallback for better error message.
domain: tld.domain ?? tld.hostname ?? "EMPTY DOMAIN",
publicSuffix: tld.publicSuffix,
});
}

View File

@@ -1003,7 +1003,8 @@ let needSetup = false;
}
});
socket.on("checkMointor", async (partial, callback) => {
// partial { type, url, hostname, grpcUrl }
socket.on("checkDomain", async (partial, callback) => {
try {
checkLogin(socket);
const DomainExpiry = require("./model/domain_expiry");

View File

@@ -1407,9 +1407,7 @@
"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_public_suffix_too_short": "\".{publicSuffix}\" is too short for a top level domain",
"domain_expiry_unsupported_is_icann": "The domain \"{domain}\" is not a candidate for domain expiry monitoring, because its public suffix \".{publicSuffix}\" is not ICAN",
"domain_expiry_unsupported_is_ip": "\"{hostname}\" is an IP address. Domain expiry monitoring requires a domain name",
"domain_expiry_unsupported_is_icann": "The domain \"{domain}\" is not a candidate for domain expiry monitoring, because its public suffix \".{publicSuffix}\" is not managed by ICANN",
"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.",

View File

@@ -1548,15 +1548,17 @@
v-model="monitor.domainExpiryNotification"
class="form-check-input"
type="checkbox"
:disabled="!hasDomain"
/>
<label class="form-check-label" for="domain-expiry-notification">
{{ $t("labelDomainNameExpiryNotification") }}
</label>
<div v-if="hasDomain" class="form-text">
<div class="form-text">
{{ $t("domainExpiryNotificationHelp") }}
</div>
<div v-if="!hasDomain && domainExpiryUnsupportedReason" class="form-text">
<div
v-if="monitor.domainExpiryNotification && domainExpiryUnsupportedReason"
class="form-text"
>
{{ domainExpiryUnsupportedReason }}
</div>
</div>
@@ -2867,7 +2869,7 @@ const monitorDefaults = {
ignoreTls: false,
upsideDown: false,
expiryNotification: false,
domainExpiryNotification: false,
domainExpiryNotification: true,
maxredirects: 10,
accepted_statuscodes: defaultValueList.http.accepted_statuscodes,
saveResponse: false,
@@ -2929,9 +2931,8 @@ export default {
notificationIDList: {},
// Do not add default value here, please check init() method
},
hasDomain: false,
domainExpiryUnsupportedReason: null,
checkMonitorDebounce: null,
checkDomainDebounce: null,
acceptedStatusCodeOptions: [],
acceptedWebsocketCodeOptions: [],
dnsresolvetypeOptions: [],
@@ -2990,16 +2991,6 @@ export default {
return this.$t("defaultFriendlyName");
},
monitorTypeUrlHost() {
const { type, url, hostname, grpcUrl } = this.monitor;
return {
type,
url,
hostname,
grpcUrl,
};
},
showDomainExpiryNotification() {
return this.monitor.type in TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD;
},
@@ -3293,30 +3284,25 @@ message HealthCheckResponse {
}
},
monitorTypeUrlHost(data) {
if (this.checkMonitorDebounce != null) {
clearTimeout(this.checkMonitorDebounce);
}
showDomainExpiryNotification() {
this.checkDomain();
},
if (!this.showDomainExpiryNotification) {
this.hasDomain = false;
this.domainExpiryUnsupportedReason = null;
return;
}
"monitor.hostname"() {
this.checkDomain();
},
this.checkMonitorDebounce = setTimeout(() => {
this.$root.getSocket().emit("checkMointor", data, (res) => {
const wasSupported = this.hasDomain;
this.hasDomain = !!res?.ok;
if (this.hasDomain !== wasSupported) {
this.monitor.domainExpiryNotification = this.hasDomain;
}
this.domainExpiryUnsupportedReason = res.msgi18n ? this.$t(res.msg, res.meta) : res.msg;
});
}, 500);
"monitor.url"() {
this.checkDomain();
},
"monitor.grpcUrl"() {
this.checkDomain();
},
"monitor.type"(newType, oldType) {
this.checkDomain();
if (newType === "globalping" && !this.monitor.subtype) {
this.monitor.subtype = "ping";
}
@@ -4015,6 +4001,39 @@ message HealthCheckResponse {
}
}
},
// Check Domain
// Do nothing if not checked
checkDomain() {
console.log("checkDomain called");
if (this.checkDomainDebounce != null) {
clearTimeout(this.checkDomainDebounce);
}
if (!this.showDomainExpiryNotification) {
this.domainExpiryUnsupportedReason = null;
return;
}
this.checkDomainDebounce = setTimeout(() => {
const { type, url, hostname, grpcUrl } = this.monitor;
const data = {
type,
url,
hostname,
grpcUrl,
};
this.$root.getSocket().emit("checkDomain", data, (res) => {
console.log(data);
if (!res.ok) {
this.domainExpiryUnsupportedReason = res.msgi18n ? this.$t(res.msg, res.meta) : res.msg;
} else {
this.domainExpiryUnsupportedReason = null;
}
});
}, 500);
},
},
};
</script>

View File

@@ -96,38 +96,6 @@ describe("Domain Expiry", () => {
});
describe("Domain Parsing", () => {
test("throws error for IP address (isIp check)", async () => {
const monitor = {
type: "http",
url: "https://127.0.0.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_is_ip");
return true;
}
);
});
test("throws error for too short suffix(example.a)", async () => {
const monitor = {
type: "http",
url: "https://example.a",
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;
}
);
});
test("throws error for non-ICANN TLD (e.g. .local)", async () => {
const monitor = {
type: "http",

View File

@@ -6,7 +6,7 @@ test.describe("Domain Expiry Notification", () => {
await restoreSqliteSnapshot(page);
});
test("supported TLD auto-enables checkbox", async ({ page }, testInfo) => {
test("TLD enabled for new monitor", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);
@@ -17,88 +17,6 @@ test.describe("Domain Expiry Notification", () => {
const checkbox = page.getByLabel("Domain Name Expiry Notification");
await expect(checkbox).toBeChecked();
await expect(checkbox).toBeEnabled();
await screenshot(testInfo, page);
});
test("unsupported TLD leaves checkbox disabled", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);
const monitorTypeSelect = page.getByTestId("monitor-type-select");
await monitorTypeSelect.selectOption("http");
await page.getByTestId("url-input").fill("https://example.co");
const checkbox = page.getByLabel("Domain Name Expiry Notification");
await expect(checkbox).not.toBeChecked();
await expect(checkbox).toBeDisabled();
await screenshot(testInfo, page);
});
test("switching from supported to unsupported TLD disables checkbox", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);
const monitorTypeSelect = page.getByTestId("monitor-type-select");
await monitorTypeSelect.selectOption("http");
const urlInput = page.getByTestId("url-input");
const checkbox = page.getByLabel("Domain Name Expiry Notification");
await urlInput.fill("https://example.com");
await expect(checkbox).toBeChecked();
await urlInput.fill("https://example.co");
await expect(checkbox).not.toBeChecked();
await expect(checkbox).toBeDisabled();
await screenshot(testInfo, page);
});
test("switching from unsupported to supported TLD enables checkbox", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);
const monitorTypeSelect = page.getByTestId("monitor-type-select");
await monitorTypeSelect.selectOption("http");
const urlInput = page.getByTestId("url-input");
const checkbox = page.getByLabel("Domain Name Expiry Notification");
await urlInput.fill("https://example.co");
await expect(checkbox).not.toBeChecked();
await urlInput.fill("https://example.com");
await expect(checkbox).toBeChecked();
await expect(checkbox).toBeEnabled();
await screenshot(testInfo, page);
});
test("manual uncheck preserved when URL changes within same TLD", async ({ page }, testInfo) => {
await page.goto("./add");
await login(page);
const monitorTypeSelect = page.getByTestId("monitor-type-select");
await monitorTypeSelect.selectOption("http");
const urlInput = page.getByTestId("url-input");
const checkbox = page.getByLabel("Domain Name Expiry Notification");
await urlInput.fill("https://example.com");
await expect(checkbox).toBeChecked();
await checkbox.uncheck();
await expect(checkbox).not.toBeChecked();
await urlInput.fill("https://example.com/different-path");
// Wait for debounce to fire and verify checkbox stays unchecked
await page.waitForTimeout(600);
await expect(checkbox).not.toBeChecked();
await expect(checkbox).toBeEnabled();
await screenshot(testInfo, page);
});