chore: made code more robust to undefined expiry (#6625)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Frank Elsinga
2026-01-06 23:39:11 +01:00
committed by GitHub
parent fc832d0935
commit 88e7887a87
2 changed files with 111 additions and 1 deletions

View File

@@ -222,6 +222,11 @@ class DomainExpiry extends BeanModel {
log.debug("domain_expiry", "No notification, no need to send domain notification");
return;
}
// 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`);
return;
}
const daysRemaining = getDaysRemaining(new Date(), domain.expiry);
const lastSent = domain.lastExpiryNotificationSent;

View File

@@ -1,6 +1,6 @@
process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(",");
const { describe, test } = require("node:test");
const { describe, test, mock } = require("node:test");
const assert = require("node:assert");
const DomainExpiry = require("../../server/model/domain_expiry");
const mockWebhook = require("./notification-providers/mock-webhook");
@@ -70,4 +70,109 @@ describe("Domain Expiry", () => {
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
const mockDomain = {
domain: "test-null.com",
expiry: null,
lastExpiryNotificationSent: null
};
mock.method(DomainExpiry, "forMonitor", async () => mockDomain);
try {
const hook = {
"port": 3012,
"url": "should-not-be-called-null"
};
const monTest = {
type: "http",
url: "https://test-null.com",
domainExpiryNotification: true
};
const notif = {
name: "TestNullExpiry",
config: JSON.stringify({
type: "webhook",
httpMethod: "post",
webhookContentType: "json",
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
})
};
// 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 ]),
mockWebhook(hook.port, hook.url, 500).then(() => {
throw new Error("Webhook was called but should not have been for null expiry");
}).catch((e) => {
if (e.reason === "Timeout") {
return "timeout"; // Expected - webhook was not called
}
throw e;
})
]);
assert.ok(result === undefined || result === "timeout", "Should not send notification for null expiry");
} finally {
mock.restoreAll();
}
});
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)
const mockDomain = {
domain: "test-undefined.com",
expiry: undefined,
lastExpiryNotificationSent: null
};
mock.method(DomainExpiry, "forMonitor", 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({
type: "webhook",
httpMethod: "post",
webhookContentType: "json",
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
})
};
// 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 ]),
mockWebhook(hook.port, hook.url, 500).then(() => {
throw new Error("Webhook was called but should not have been for undefined expiry");
}).catch((e) => {
if (e.reason === "Timeout") {
return "timeout"; // Expected - webhook was not called
}
throw e;
})
]);
assert.ok(result === undefined || result === "timeout", "Should not send notification for undefined expiry");
} finally {
mock.restoreAll();
}
});
});