Skip to content

Commit 88e7887

Browse files
chore: made code more robust to undefined expiry (#6625)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fc832d0 commit 88e7887

2 files changed

Lines changed: 111 additions & 1 deletion

File tree

server/model/domain_expiry.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ class DomainExpiry extends BeanModel {
222222
log.debug("domain_expiry", "No notification, no need to send domain notification");
223223
return;
224224
}
225+
// sanity check if expiry date is valid before calculating days remaining. Should not happen and likely indicates a bug in the code.
226+
if (!domain.expiry || isNaN(new Date(domain.expiry).getTime())) {
227+
log.warn("domain_expiry", `No valid expiry date passed to sendNotifications for ${name} (expiry: ${domain.expiry}), skipping notification`);
228+
return;
229+
}
225230

226231
const daysRemaining = getDaysRemaining(new Date(), domain.expiry);
227232
const lastSent = domain.lastExpiryNotificationSent;

test/backend-test/test-domain.js

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(",");
22

3-
const { describe, test } = require("node:test");
3+
const { describe, test, mock } = require("node:test");
44
const assert = require("node:assert");
55
const DomainExpiry = require("../../server/model/domain_expiry");
66
const mockWebhook = require("./notification-providers/mock-webhook");
@@ -70,4 +70,109 @@ describe("Domain Expiry", () => {
7070
await testDb.destroy();
7171
}, 200);
7272
});
73+
74+
test("sendNotifications() handles domain with null expiry without sending NaN", async () => {
75+
// Regression test for bug: "Domain name will expire in NaN days"
76+
// Mock forMonitor to return a bean with null expiry
77+
const mockDomain = {
78+
domain: "test-null.com",
79+
expiry: null,
80+
lastExpiryNotificationSent: null
81+
};
82+
83+
mock.method(DomainExpiry, "forMonitor", async () => mockDomain);
84+
85+
try {
86+
const hook = {
87+
"port": 3012,
88+
"url": "should-not-be-called-null"
89+
};
90+
91+
const monTest = {
92+
type: "http",
93+
url: "https://test-null.com",
94+
domainExpiryNotification: true
95+
};
96+
97+
const notif = {
98+
name: "TestNullExpiry",
99+
config: JSON.stringify({
100+
type: "webhook",
101+
httpMethod: "post",
102+
webhookContentType: "json",
103+
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
104+
})
105+
};
106+
107+
// Race between sendNotifications and mockWebhook timeout
108+
// If webhook is called, we fail. If it times out, we pass.
109+
const result = await Promise.race([
110+
DomainExpiry.sendNotifications(monTest, [ notif ]),
111+
mockWebhook(hook.port, hook.url, 500).then(() => {
112+
throw new Error("Webhook was called but should not have been for null expiry");
113+
}).catch((e) => {
114+
if (e.reason === "Timeout") {
115+
return "timeout"; // Expected - webhook was not called
116+
}
117+
throw e;
118+
})
119+
]);
120+
121+
assert.ok(result === undefined || result === "timeout", "Should not send notification for null expiry");
122+
} finally {
123+
mock.restoreAll();
124+
}
125+
});
126+
127+
test("sendNotifications() handles domain with undefined expiry without sending NaN", async () => {
128+
try {
129+
// Mock forMonitor to return a bean with undefined expiry (newly created bean scenario)
130+
const mockDomain = {
131+
domain: "test-undefined.com",
132+
expiry: undefined,
133+
lastExpiryNotificationSent: null
134+
};
135+
136+
mock.method(DomainExpiry, "forMonitor", async () => mockDomain);
137+
138+
const hook = {
139+
"port": 3013,
140+
"url": "should-not-be-called-undefined"
141+
};
142+
143+
const monTest = {
144+
type: "http",
145+
url: "https://test-undefined.com",
146+
domainExpiryNotification: true
147+
};
148+
149+
const notif = {
150+
name: "TestUndefinedExpiry",
151+
config: JSON.stringify({
152+
type: "webhook",
153+
httpMethod: "post",
154+
webhookContentType: "json",
155+
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
156+
})
157+
};
158+
159+
// Race between sendNotifications and mockWebhook timeout
160+
// If webhook is called, we fail. If it times out, we pass.
161+
const result = await Promise.race([
162+
DomainExpiry.sendNotifications(monTest, [ notif ]),
163+
mockWebhook(hook.port, hook.url, 500).then(() => {
164+
throw new Error("Webhook was called but should not have been for undefined expiry");
165+
}).catch((e) => {
166+
if (e.reason === "Timeout") {
167+
return "timeout"; // Expected - webhook was not called
168+
}
169+
throw e;
170+
})
171+
]);
172+
173+
assert.ok(result === undefined || result === "timeout", "Should not send notification for undefined expiry");
174+
} finally {
175+
mock.restoreAll();
176+
}
177+
});
73178
});

0 commit comments

Comments
 (0)