From 2785ddf522c84ef78c55f6442132b4a0a07f582a Mon Sep 17 00:00:00 2001 From: cyril59310 Date: Sun, 28 Dec 2025 03:16:32 +0100 Subject: [PATCH 01/35] default icons for status page --- src/lang/en.json | 3 +- src/pages/StatusPage.vue | 70 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/lang/en.json b/src/lang/en.json index 727358fdc..4b7511758 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1246,5 +1246,6 @@ "labelDomainNameExpiryNotification": "Domain Name Expiry Notification", "domainExpiryDescription": "Trigger notification when domain names expires in:", "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." + "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.", + "Image reset to default": "Image reset to default" } diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 74a5c3f66..f2fcebf7c 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -138,6 +138,9 @@

+ @@ -962,6 +965,21 @@ export default { } }, + /** + * Reset logo image to default (public/icon.svg) + * @returns {void} + */ + resetToDefaultImage() { + if (! this.editMode) { + return; + } + + this.imgDataUrl = "/icon.svg"; + this.config.icon = this.imgDataUrl; + toast.success(this.$t("Image reset to default")); + }, + + /** * Create an incident for this status page * @returns {void} @@ -1181,6 +1199,58 @@ footer { cursor: pointer; box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); } + + /* Reset button placed at top-left of the logo */ + .reset-top-left { + position: absolute; + top: 0; + left: -15px; + z-index: 2; + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: white; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + cursor: pointer; + padding: 0; + transition: transform $easing-in 0.18s, box-shadow $easing-in 0.18s, background-color $easing-in 0.18s; + transform-origin: center; + + &:hover { + background-color: rgba(0, 0, 0, 0.06); + transform: scale(1.18); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); + } + + &:hover ~ .icon-upload { + transform: none !important; + } + } + + .small-reset-btn { + transition: transform $easing-in 0.18s, box-shadow $easing-in 0.18s, background-color $easing-in 0.18s; + font-size: 18px; + width: 18px; + height: 18px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: transparent; + border: none; + cursor: pointer; + + &:hover { + background-color: rgba(0, 0, 0, 0.04); + transform: scale(1.18); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); + } + } } .logo { From ab61f8dc89dbc644627b0be85c7b91de852d82fe Mon Sep 17 00:00:00 2001 From: cyril59310 Date: Sun, 28 Dec 2025 03:46:40 +0100 Subject: [PATCH 02/35] fix --- src/pages/StatusPage.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index f2fcebf7c..b2bcb1c52 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -979,7 +979,6 @@ export default { toast.success(this.$t("Image reset to default")); }, - /** * Create an incident for this status page * @returns {void} From 875dd1288a6a21de4bc5dfc00e65594cde2f5139 Mon Sep 17 00:00:00 2001 From: cyril59310 Date: Sun, 28 Dec 2025 20:30:54 +0100 Subject: [PATCH 03/35] translation key update --- src/lang/en.json | 2 +- src/pages/StatusPage.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/en.json b/src/lang/en.json index 4b7511758..c6c410c23 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1247,5 +1247,5 @@ "domainExpiryDescription": "Trigger notification when domain names expires in:", "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.", - "Image reset to default": "Image reset to default" + "imageResetConfirmation": "Image reset to default" } diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index b2bcb1c52..7b2426a3c 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -976,7 +976,7 @@ export default { this.imgDataUrl = "/icon.svg"; this.config.icon = this.imgDataUrl; - toast.success(this.$t("Image reset to default")); + toast.success(this.$t("imageResetConfirmation")); }, /** From c9381154a6013fedd3923f1201f4f4c5f301c4ba Mon Sep 17 00:00:00 2001 From: DanielDerefaka Date: Thu, 1 Jan 2026 11:41:11 +0100 Subject: [PATCH 04/35] feat(status-page): add og:type and og:image Open Graph meta tags Added additional Open Graph meta tags to status pages for better social media sharing: - og:type (set to "website") - og:image (uses the status page icon if configured) This complements the existing og:title and og:description tags. Fixes #6514 --- server/model/status_page.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/model/status_page.js b/server/model/status_page.js index 224441127..9842f0019 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -132,6 +132,14 @@ class StatusPage extends BeanModel { let ogDescription = $("").attr("content", description155); head.append(ogDescription); + let ogType = $(""); + head.append(ogType); + + if (statusPage.icon) { + let ogImage = $("").attr("content", statusPage.icon); + head.append(ogImage); + } + // Preload data // Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186 const escapedJSONObject = jsesc(await StatusPage.getStatusPageData(statusPage), { From bcfd1e929558c58b926680f420a2ea8687387612 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 15:41:07 +0100 Subject: [PATCH 05/35] group related components --- .../monitor-conditions/test-evaluator.js | 75 ++++---- .../monitor-conditions/test-operators.js | 180 +++++++++--------- 2 files changed, 130 insertions(+), 125 deletions(-) diff --git a/test/backend-test/monitor-conditions/test-evaluator.js b/test/backend-test/monitor-conditions/test-evaluator.js index da7c7fabf..fd74fa7d2 100644 --- a/test/backend-test/monitor-conditions/test-evaluator.js +++ b/test/backend-test/monitor-conditions/test-evaluator.js @@ -1,46 +1,49 @@ + const test = require("node:test"); const assert = require("node:assert"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js"); -test("Test evaluateExpression", async (t) => { - const expr = new ConditionExpression("record", "contains", "mx1.example.com"); - assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); - assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); -}); +test("evaluateExpression", async (t) => { + await t.test("Test evaluateExpression", async (t) => { + const expr = new ConditionExpression("record", "contains", "mx1.example.com"); + assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); + assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); + }); -test("Test evaluateExpressionGroup with logical AND", async (t) => { - const group = new ConditionExpressionGroup([ - new ConditionExpression("record", "contains", "mx1."), - new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), - ]); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); -}); + await t.test("Test evaluateExpressionGroup with logical AND", async (t) => { + const group = new ConditionExpressionGroup([ + new ConditionExpression("record", "contains", "mx1."), + new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), + ]); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); + }); -test("Test evaluateExpressionGroup with logical OR", async (t) => { - const group = new ConditionExpressionGroup([ - new ConditionExpression("record", "contains", "example.com"), - new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), - ]); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" })); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" })); -}); - -test("Test evaluateExpressionGroup with nested group", async (t) => { - const group = new ConditionExpressionGroup([ - new ConditionExpression("record", "contains", "mx1."), - new ConditionExpressionGroup([ + await t.test("Test evaluateExpressionGroup with logical OR", async (t) => { + const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "example.com"), new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), - ]), - ]); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); - assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" })); - assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" })); + ]); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" })); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" })); + }); + + await t.test("Test evaluateExpressionGroup with nested group", async (t) => { + const group = new ConditionExpressionGroup([ + new ConditionExpression("record", "contains", "mx1."), + new ConditionExpressionGroup([ + new ConditionExpression("record", "contains", "example.com"), + new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), + ]), + ]); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); + assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" })); + assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" })); + }); }); diff --git a/test/backend-test/monitor-conditions/test-operators.js b/test/backend-test/monitor-conditions/test-operators.js index e663c9a50..66bf07e67 100644 --- a/test/backend-test/monitor-conditions/test-operators.js +++ b/test/backend-test/monitor-conditions/test-operators.js @@ -2,107 +2,109 @@ const test = require("node:test"); const assert = require("node:assert"); const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js"); -test("Test StringEqualsOperator", async (t) => { - const op = operatorMap.get(OP_STR_EQUALS); - assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); - assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); - assert.strictEqual(false, op.test("1", 1)); // strict equality -}); +test("expressionOperators", async (t) => { + await t.test("Test StringEqualsOperator", async (t) => { + const op = operatorMap.get(OP_STR_EQUALS); + assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); + assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); + assert.strictEqual(false, op.test("1", 1)); // strict equality + }); -test("Test StringNotEqualsOperator", async (t) => { - const op = operatorMap.get(OP_STR_NOT_EQUALS); - assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); - assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); - assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) -}); + await t.test("Test StringNotEqualsOperator", async (t) => { + const op = operatorMap.get(OP_STR_NOT_EQUALS); + assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); + assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); + assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) + }); -test("Test ContainsOperator with scalar", async (t) => { - const op = operatorMap.get(OP_CONTAINS); - assert.strictEqual(true, op.test("mx1.example.org", "example.org")); - assert.strictEqual(false, op.test("mx1.example.org", "example.com")); -}); + await t.test("Test ContainsOperator with scalar", async (t) => { + const op = operatorMap.get(OP_CONTAINS); + assert.strictEqual(true, op.test("mx1.example.org", "example.org")); + assert.strictEqual(false, op.test("mx1.example.org", "example.com")); + }); -test("Test ContainsOperator with array", async (t) => { - const op = operatorMap.get(OP_CONTAINS); - assert.strictEqual(true, op.test([ "example.org" ], "example.org")); - assert.strictEqual(false, op.test([ "example.org" ], "example.com")); -}); + await t.test("Test ContainsOperator with array", async (t) => { + const op = operatorMap.get(OP_CONTAINS); + assert.strictEqual(true, op.test([ "example.org" ], "example.org")); + assert.strictEqual(false, op.test([ "example.org" ], "example.com")); + }); -test("Test NotContainsOperator with scalar", async (t) => { - const op = operatorMap.get(OP_NOT_CONTAINS); - assert.strictEqual(true, op.test("example.org", ".com")); - assert.strictEqual(false, op.test("example.org", ".org")); -}); + await t.test("Test NotContainsOperator with scalar", async (t) => { + const op = operatorMap.get(OP_NOT_CONTAINS); + assert.strictEqual(true, op.test("example.org", ".com")); + assert.strictEqual(false, op.test("example.org", ".org")); + }); -test("Test NotContainsOperator with array", async (t) => { - const op = operatorMap.get(OP_NOT_CONTAINS); - assert.strictEqual(true, op.test([ "example.org" ], "example.com")); - assert.strictEqual(false, op.test([ "example.org" ], "example.org")); -}); + await t.test("Test NotContainsOperator with array", async (t) => { + const op = operatorMap.get(OP_NOT_CONTAINS); + assert.strictEqual(true, op.test([ "example.org" ], "example.com")); + assert.strictEqual(false, op.test([ "example.org" ], "example.org")); + }); -test("Test StartsWithOperator", async (t) => { - const op = operatorMap.get(OP_STARTS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "mx1")); - assert.strictEqual(false, op.test("mx1.example.com", "mx2")); -}); + await t.test("Test StartsWithOperator", async (t) => { + const op = operatorMap.get(OP_STARTS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "mx1")); + assert.strictEqual(false, op.test("mx1.example.com", "mx2")); + }); -test("Test NotStartsWithOperator", async (t) => { - const op = operatorMap.get(OP_NOT_STARTS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "mx2")); - assert.strictEqual(false, op.test("mx1.example.com", "mx1")); -}); + await t.test("Test NotStartsWithOperator", async (t) => { + const op = operatorMap.get(OP_NOT_STARTS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "mx2")); + assert.strictEqual(false, op.test("mx1.example.com", "mx1")); + }); -test("Test EndsWithOperator", async (t) => { - const op = operatorMap.get(OP_ENDS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "example.com")); - assert.strictEqual(false, op.test("mx1.example.com", "example.net")); -}); + await t.test("Test EndsWithOperator", async (t) => { + const op = operatorMap.get(OP_ENDS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "example.com")); + assert.strictEqual(false, op.test("mx1.example.com", "example.net")); + }); -test("Test NotEndsWithOperator", async (t) => { - const op = operatorMap.get(OP_NOT_ENDS_WITH); - assert.strictEqual(true, op.test("mx1.example.com", "example.net")); - assert.strictEqual(false, op.test("mx1.example.com", "example.com")); -}); + await t.test("Test NotEndsWithOperator", async (t) => { + const op = operatorMap.get(OP_NOT_ENDS_WITH); + assert.strictEqual(true, op.test("mx1.example.com", "example.net")); + assert.strictEqual(false, op.test("mx1.example.com", "example.com")); + }); -test("Test NumberEqualsOperator", async (t) => { - const op = operatorMap.get(OP_NUM_EQUALS); - assert.strictEqual(true, op.test(1, 1)); - assert.strictEqual(true, op.test(1, "1")); - assert.strictEqual(false, op.test(1, "2")); -}); + await t.test("Test NumberEqualsOperator", async (t) => { + const op = operatorMap.get(OP_NUM_EQUALS); + assert.strictEqual(true, op.test(1, 1)); + assert.strictEqual(true, op.test(1, "1")); + assert.strictEqual(false, op.test(1, "2")); + }); -test("Test NumberNotEqualsOperator", async (t) => { - const op = operatorMap.get(OP_NUM_NOT_EQUALS); - assert.strictEqual(true, op.test(1, "2")); - assert.strictEqual(false, op.test(1, "1")); -}); + await t.test("Test NumberNotEqualsOperator", async (t) => { + const op = operatorMap.get(OP_NUM_NOT_EQUALS); + assert.strictEqual(true, op.test(1, "2")); + assert.strictEqual(false, op.test(1, "1")); + }); -test("Test LessThanOperator", async (t) => { - const op = operatorMap.get(OP_LT); - assert.strictEqual(true, op.test(1, 2)); - assert.strictEqual(true, op.test(1, "2")); - assert.strictEqual(false, op.test(1, 1)); -}); + await t.test("Test LessThanOperator", async (t) => { + const op = operatorMap.get(OP_LT); + assert.strictEqual(true, op.test(1, 2)); + assert.strictEqual(true, op.test(1, "2")); + assert.strictEqual(false, op.test(1, 1)); + }); -test("Test GreaterThanOperator", async (t) => { - const op = operatorMap.get(OP_GT); - assert.strictEqual(true, op.test(2, 1)); - assert.strictEqual(true, op.test(2, "1")); - assert.strictEqual(false, op.test(1, 1)); -}); + await t.test("Test GreaterThanOperator", async (t) => { + const op = operatorMap.get(OP_GT); + assert.strictEqual(true, op.test(2, 1)); + assert.strictEqual(true, op.test(2, "1")); + assert.strictEqual(false, op.test(1, 1)); + }); -test("Test LessThanOrEqualToOperator", async (t) => { - const op = operatorMap.get(OP_LTE); - assert.strictEqual(true, op.test(1, 1)); - assert.strictEqual(true, op.test(1, 2)); - assert.strictEqual(true, op.test(1, "2")); - assert.strictEqual(false, op.test(1, 0)); -}); + await t.test("Test LessThanOrEqualToOperator", async (t) => { + const op = operatorMap.get(OP_LTE); + assert.strictEqual(true, op.test(1, 1)); + assert.strictEqual(true, op.test(1, 2)); + assert.strictEqual(true, op.test(1, "2")); + assert.strictEqual(false, op.test(1, 0)); + }); -test("Test GreaterThanOrEqualToOperator", async (t) => { - const op = operatorMap.get(OP_GTE); - assert.strictEqual(true, op.test(1, 1)); - assert.strictEqual(true, op.test(2, 1)); - assert.strictEqual(true, op.test(2, "2")); - assert.strictEqual(false, op.test(2, 3)); -}); + await t.test("Test GreaterThanOrEqualToOperator", async (t) => { + const op = operatorMap.get(OP_GTE); + assert.strictEqual(true, op.test(1, 1)); + assert.strictEqual(true, op.test(2, 1)); + assert.strictEqual(true, op.test(2, "2")); + assert.strictEqual(false, op.test(2, 3)); + }); +}); \ No newline at end of file From 0901434c9fbba1b44662fce431d6686c027208e1 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:16:45 +0100 Subject: [PATCH 06/35] fix the conditions tests being weirder than they need to --- .../monitor-conditions/test-evaluator.js | 10 +++--- .../monitor-conditions/test-operators.js | 36 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/test/backend-test/monitor-conditions/test-evaluator.js b/test/backend-test/monitor-conditions/test-evaluator.js index fd74fa7d2..edd051abd 100644 --- a/test/backend-test/monitor-conditions/test-evaluator.js +++ b/test/backend-test/monitor-conditions/test-evaluator.js @@ -4,14 +4,14 @@ const assert = require("node:assert"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js"); -test("evaluateExpression", async (t) => { - await t.test("Test evaluateExpression", async (t) => { +test("Expression Evaluator", async (t) => { + await t.test("evaluateExpression() returns true when condition matches and false otherwise", async (t) => { const expr = new ConditionExpression("record", "contains", "mx1.example.com"); assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); }); - await t.test("Test evaluateExpressionGroup with logical AND", async (t) => { + await t.test("evaluateExpressionGroup() with AND logic requires all conditions to be true", async (t) => { const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "mx1."), new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), @@ -21,7 +21,7 @@ test("evaluateExpression", async (t) => { assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); }); - await t.test("Test evaluateExpressionGroup with logical OR", async (t) => { + await t.test("evaluateExpressionGroup() with OR logic requires at least one condition to be true", async (t) => { const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "example.com"), new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), @@ -31,7 +31,7 @@ test("evaluateExpression", async (t) => { assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" })); }); - await t.test("Test evaluateExpressionGroup with nested group", async (t) => { + await t.test("evaluateExpressionGroup() evaluates nested groups correctly", async (t) => { const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "mx1."), new ConditionExpressionGroup([ diff --git a/test/backend-test/monitor-conditions/test-operators.js b/test/backend-test/monitor-conditions/test-operators.js index 66bf07e67..ef10d63a2 100644 --- a/test/backend-test/monitor-conditions/test-operators.js +++ b/test/backend-test/monitor-conditions/test-operators.js @@ -2,97 +2,97 @@ const test = require("node:test"); const assert = require("node:assert"); const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js"); -test("expressionOperators", async (t) => { - await t.test("Test StringEqualsOperator", async (t) => { +test("Expression Operators", async (t) => { + await t.test("StringEqualsOperator returns true for identical strings and false otherwise", async (t) => { const op = operatorMap.get(OP_STR_EQUALS); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(false, op.test("1", 1)); // strict equality }); - await t.test("Test StringNotEqualsOperator", async (t) => { + await t.test("StringNotEqualsOperator returns true for different strings and false for identical strings", async (t) => { const op = operatorMap.get(OP_STR_NOT_EQUALS); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) }); - await t.test("Test ContainsOperator with scalar", async (t) => { + await t.test("ContainsOperator returns true when scalar contains substring", async (t) => { const op = operatorMap.get(OP_CONTAINS); assert.strictEqual(true, op.test("mx1.example.org", "example.org")); assert.strictEqual(false, op.test("mx1.example.org", "example.com")); }); - await t.test("Test ContainsOperator with array", async (t) => { + await t.test("ContainsOperator returns true when array contains element", async (t) => { const op = operatorMap.get(OP_CONTAINS); assert.strictEqual(true, op.test([ "example.org" ], "example.org")); assert.strictEqual(false, op.test([ "example.org" ], "example.com")); }); - await t.test("Test NotContainsOperator with scalar", async (t) => { + await t.test("NotContainsOperator returns true when scalar does not contain substring", async (t) => { const op = operatorMap.get(OP_NOT_CONTAINS); assert.strictEqual(true, op.test("example.org", ".com")); assert.strictEqual(false, op.test("example.org", ".org")); }); - await t.test("Test NotContainsOperator with array", async (t) => { + await t.test("NotContainsOperator returns true when array does not contain element", async (t) => { const op = operatorMap.get(OP_NOT_CONTAINS); assert.strictEqual(true, op.test([ "example.org" ], "example.com")); assert.strictEqual(false, op.test([ "example.org" ], "example.org")); }); - await t.test("Test StartsWithOperator", async (t) => { + await t.test("StartsWithOperator returns true when string starts with prefix", async (t) => { const op = operatorMap.get(OP_STARTS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "mx1")); assert.strictEqual(false, op.test("mx1.example.com", "mx2")); }); - await t.test("Test NotStartsWithOperator", async (t) => { + await t.test("NotStartsWithOperator returns true when string does not start with prefix", async (t) => { const op = operatorMap.get(OP_NOT_STARTS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "mx2")); assert.strictEqual(false, op.test("mx1.example.com", "mx1")); }); - await t.test("Test EndsWithOperator", async (t) => { + await t.test("EndsWithOperator returns true when string ends with suffix", async (t) => { const op = operatorMap.get(OP_ENDS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "example.com")); assert.strictEqual(false, op.test("mx1.example.com", "example.net")); }); - await t.test("Test NotEndsWithOperator", async (t) => { + await t.test("NotEndsWithOperator returns true when string does not end with suffix", async (t) => { const op = operatorMap.get(OP_NOT_ENDS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "example.net")); assert.strictEqual(false, op.test("mx1.example.com", "example.com")); }); - await t.test("Test NumberEqualsOperator", async (t) => { + await t.test("NumberEqualsOperator returns true for equal numbers with type coercion", async (t) => { const op = operatorMap.get(OP_NUM_EQUALS); assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, "1")); assert.strictEqual(false, op.test(1, "2")); }); - await t.test("Test NumberNotEqualsOperator", async (t) => { + await t.test("NumberNotEqualsOperator returns true for different numbers", async (t) => { const op = operatorMap.get(OP_NUM_NOT_EQUALS); assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(false, op.test(1, "1")); }); - await t.test("Test LessThanOperator", async (t) => { + await t.test("LessThanOperator returns true when first number is less than second", async (t) => { const op = operatorMap.get(OP_LT); assert.strictEqual(true, op.test(1, 2)); assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(false, op.test(1, 1)); }); - await t.test("Test GreaterThanOperator", async (t) => { + await t.test("GreaterThanOperator returns true when first number is greater than second", async (t) => { const op = operatorMap.get(OP_GT); assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, "1")); assert.strictEqual(false, op.test(1, 1)); }); - await t.test("Test LessThanOrEqualToOperator", async (t) => { + await t.test("LessThanOrEqualToOperator returns true when first number is less than or equal to second", async (t) => { const op = operatorMap.get(OP_LTE); assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 2)); @@ -100,11 +100,11 @@ test("expressionOperators", async (t) => { assert.strictEqual(false, op.test(1, 0)); }); - await t.test("Test GreaterThanOrEqualToOperator", async (t) => { + await t.test("GreaterThanOrEqualToOperator returns true when first number is greater than or equal to second", async (t) => { const op = operatorMap.get(OP_GTE); assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, "2")); assert.strictEqual(false, op.test(2, 3)); }); -}); \ No newline at end of file +}); From e4c347be0b61ede6fa18227f4417455315b58a1c Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:22:17 +0100 Subject: [PATCH 07/35] make sure we use test suites --- .../monitor-conditions/test-evaluator.js | 12 +++---- .../monitor-conditions/test-operators.js | 36 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/test/backend-test/monitor-conditions/test-evaluator.js b/test/backend-test/monitor-conditions/test-evaluator.js index edd051abd..f2e967b81 100644 --- a/test/backend-test/monitor-conditions/test-evaluator.js +++ b/test/backend-test/monitor-conditions/test-evaluator.js @@ -1,17 +1,17 @@ -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js"); -test("Expression Evaluator", async (t) => { - await t.test("evaluateExpression() returns true when condition matches and false otherwise", async (t) => { +describe("Expression Evaluator", () => { + test("evaluateExpression() returns true when condition matches and false otherwise", () => { const expr = new ConditionExpression("record", "contains", "mx1.example.com"); assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" })); }); - await t.test("evaluateExpressionGroup() with AND logic requires all conditions to be true", async (t) => { + test("evaluateExpressionGroup() with AND logic requires all conditions to be true", () => { const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "mx1."), new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), @@ -21,7 +21,7 @@ test("Expression Evaluator", async (t) => { assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); }); - await t.test("evaluateExpressionGroup() with OR logic requires at least one condition to be true", async (t) => { + test("evaluateExpressionGroup() with OR logic requires at least one condition to be true", () => { const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "example.com"), new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), @@ -31,7 +31,7 @@ test("Expression Evaluator", async (t) => { assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" })); }); - await t.test("evaluateExpressionGroup() evaluates nested groups correctly", async (t) => { + test("evaluateExpressionGroup() evaluates nested groups correctly", () => { const group = new ConditionExpressionGroup([ new ConditionExpression("record", "contains", "mx1."), new ConditionExpressionGroup([ diff --git a/test/backend-test/monitor-conditions/test-operators.js b/test/backend-test/monitor-conditions/test-operators.js index ef10d63a2..6a6739631 100644 --- a/test/backend-test/monitor-conditions/test-operators.js +++ b/test/backend-test/monitor-conditions/test-operators.js @@ -1,98 +1,98 @@ -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js"); -test("Expression Operators", async (t) => { - await t.test("StringEqualsOperator returns true for identical strings and false otherwise", async (t) => { +describe("Expression Operators", () => { + test("StringEqualsOperator returns true for identical strings and false otherwise", () => { const op = operatorMap.get(OP_STR_EQUALS); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(false, op.test("1", 1)); // strict equality }); - await t.test("StringNotEqualsOperator returns true for different strings and false for identical strings", async (t) => { + test("StringNotEqualsOperator returns true for different strings and false for identical strings", () => { const op = operatorMap.get(OP_STR_NOT_EQUALS); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) }); - await t.test("ContainsOperator returns true when scalar contains substring", async (t) => { + test("ContainsOperator returns true when scalar contains substring", () => { const op = operatorMap.get(OP_CONTAINS); assert.strictEqual(true, op.test("mx1.example.org", "example.org")); assert.strictEqual(false, op.test("mx1.example.org", "example.com")); }); - await t.test("ContainsOperator returns true when array contains element", async (t) => { + test("ContainsOperator returns true when array contains element", () => { const op = operatorMap.get(OP_CONTAINS); assert.strictEqual(true, op.test([ "example.org" ], "example.org")); assert.strictEqual(false, op.test([ "example.org" ], "example.com")); }); - await t.test("NotContainsOperator returns true when scalar does not contain substring", async (t) => { + test("NotContainsOperator returns true when scalar does not contain substring", () => { const op = operatorMap.get(OP_NOT_CONTAINS); assert.strictEqual(true, op.test("example.org", ".com")); assert.strictEqual(false, op.test("example.org", ".org")); }); - await t.test("NotContainsOperator returns true when array does not contain element", async (t) => { + test("NotContainsOperator returns true when array does not contain element", () => { const op = operatorMap.get(OP_NOT_CONTAINS); assert.strictEqual(true, op.test([ "example.org" ], "example.com")); assert.strictEqual(false, op.test([ "example.org" ], "example.org")); }); - await t.test("StartsWithOperator returns true when string starts with prefix", async (t) => { + test("StartsWithOperator returns true when string starts with prefix", () => { const op = operatorMap.get(OP_STARTS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "mx1")); assert.strictEqual(false, op.test("mx1.example.com", "mx2")); }); - await t.test("NotStartsWithOperator returns true when string does not start with prefix", async (t) => { + test("NotStartsWithOperator returns true when string does not start with prefix", () => { const op = operatorMap.get(OP_NOT_STARTS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "mx2")); assert.strictEqual(false, op.test("mx1.example.com", "mx1")); }); - await t.test("EndsWithOperator returns true when string ends with suffix", async (t) => { + test("EndsWithOperator returns true when string ends with suffix", () => { const op = operatorMap.get(OP_ENDS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "example.com")); assert.strictEqual(false, op.test("mx1.example.com", "example.net")); }); - await t.test("NotEndsWithOperator returns true when string does not end with suffix", async (t) => { + test("NotEndsWithOperator returns true when string does not end with suffix", () => { const op = operatorMap.get(OP_NOT_ENDS_WITH); assert.strictEqual(true, op.test("mx1.example.com", "example.net")); assert.strictEqual(false, op.test("mx1.example.com", "example.com")); }); - await t.test("NumberEqualsOperator returns true for equal numbers with type coercion", async (t) => { + test("NumberEqualsOperator returns true for equal numbers with type coercion", () => { const op = operatorMap.get(OP_NUM_EQUALS); assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, "1")); assert.strictEqual(false, op.test(1, "2")); }); - await t.test("NumberNotEqualsOperator returns true for different numbers", async (t) => { + test("NumberNotEqualsOperator returns true for different numbers", () => { const op = operatorMap.get(OP_NUM_NOT_EQUALS); assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(false, op.test(1, "1")); }); - await t.test("LessThanOperator returns true when first number is less than second", async (t) => { + test("LessThanOperator returns true when first number is less than second", () => { const op = operatorMap.get(OP_LT); assert.strictEqual(true, op.test(1, 2)); assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(false, op.test(1, 1)); }); - await t.test("GreaterThanOperator returns true when first number is greater than second", async (t) => { + test("GreaterThanOperator returns true when first number is greater than second", () => { const op = operatorMap.get(OP_GT); assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, "1")); assert.strictEqual(false, op.test(1, 1)); }); - await t.test("LessThanOrEqualToOperator returns true when first number is less than or equal to second", async (t) => { + test("LessThanOrEqualToOperator returns true when first number is less than or equal to second", () => { const op = operatorMap.get(OP_LTE); assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 2)); @@ -100,7 +100,7 @@ test("Expression Operators", async (t) => { assert.strictEqual(false, op.test(1, 0)); }); - await t.test("GreaterThanOrEqualToOperator returns true when first number is greater than or equal to second", async (t) => { + test("GreaterThanOrEqualToOperator returns true when first number is greater than or equal to second", () => { const op = operatorMap.get(OP_GTE); assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(2, 1)); From b6d5dd690d76a34ff199b7c98e43a503e935c716 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:25:18 +0100 Subject: [PATCH 08/35] make sure the calculator uses a suite --- test/backend-test/test-uptime-calculator.js | 680 ++++++++++---------- 1 file changed, 339 insertions(+), 341 deletions(-) diff --git a/test/backend-test/test-uptime-calculator.js b/test/backend-test/test-uptime-calculator.js index 4f2f05efe..622bb8f02 100644 --- a/test/backend-test/test-uptime-calculator.js +++ b/test/backend-test/test-uptime-calculator.js @@ -1,4 +1,4 @@ -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const { UptimeCalculator } = require("../../server/uptime-calculator"); const dayjs = require("dayjs"); @@ -7,344 +7,403 @@ dayjs.extend(require("dayjs/plugin/utc")); dayjs.extend(require("../../server/modules/dayjs/plugin/timezone")); dayjs.extend(require("dayjs/plugin/customParseFormat")); -test("Test Uptime Calculator - custom date", async (t) => { - let c1 = new UptimeCalculator(); +describe("Uptime Calculator", () => { + test("getCurrentDate() returns custom date when set", () => { + let c1 = new UptimeCalculator(); - // Test custom date - UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z"); - assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix()); -}); + // Test custom date + UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z"); + assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix()); + }); -test("Test update - UP", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); - let c2 = new UptimeCalculator(); - let date = await c2.update(UP); - assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix()); -}); + test("update() with UP status returns correct timestamp", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); + let c2 = new UptimeCalculator(); + let date = await c2.update(UP); + assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix()); + }); -test("Test update - MAINTENANCE", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); - let c2 = new UptimeCalculator(); - let date = await c2.update(MAINTENANCE); - assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); -}); + test("update() with MAINTENANCE status returns correct timestamp", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); + let c2 = new UptimeCalculator(); + let date = await c2.update(MAINTENANCE); + assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); + }); -test("Test update - DOWN", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); - let c2 = new UptimeCalculator(); - let date = await c2.update(DOWN); - assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); -}); + test("update() with DOWN status returns correct timestamp", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); + let c2 = new UptimeCalculator(); + let date = await c2.update(DOWN); + assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); + }); -test("Test update - PENDING", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); - let c2 = new UptimeCalculator(); - let date = await c2.update(PENDING); - assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); -}); + test("update() with PENDING status returns correct timestamp", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); + let c2 = new UptimeCalculator(); + let date = await c2.update(PENDING); + assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); + }); -test("Test flatStatus", async (t) => { - let c2 = new UptimeCalculator(); - assert.strictEqual(c2.flatStatus(UP), UP); - //assert.strictEqual(c2.flatStatus(MAINTENANCE), UP); - assert.strictEqual(c2.flatStatus(DOWN), DOWN); - assert.strictEqual(c2.flatStatus(PENDING), DOWN); -}); + test("flatStatus() converts statuses correctly", () => { + let c2 = new UptimeCalculator(); + assert.strictEqual(c2.flatStatus(UP), UP); + //assert.strictEqual(c2.flatStatus(MAINTENANCE), UP); + assert.strictEqual(c2.flatStatus(DOWN), DOWN); + assert.strictEqual(c2.flatStatus(PENDING), DOWN); + }); -test("Test getMinutelyKey", async (t) => { - let c2 = new UptimeCalculator(); - let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00")); - assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); + test("getMinutelyKey() returns correct timestamp for start of minute", () => { + let c2 = new UptimeCalculator(); + let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00")); + assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); - // Edge case 1 - c2 = new UptimeCalculator(); - divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01")); - assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); + // Edge case 1 + c2 = new UptimeCalculator(); + divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01")); + assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); - // Edge case 2 - c2 = new UptimeCalculator(); - divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59")); - assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); -}); + // Edge case 2 + c2 = new UptimeCalculator(); + divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59")); + assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); + }); -test("Test getDailyKey", async (t) => { - let c2 = new UptimeCalculator(); - let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00")); - assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); + test("getDailyKey() returns correct timestamp for start of day", () => { + let c2 = new UptimeCalculator(); + let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00")); + assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); - c2 = new UptimeCalculator(); - dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30")); - assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); + c2 = new UptimeCalculator(); + dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30")); + assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); - // Edge case 1 - c2 = new UptimeCalculator(); - dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59")); - assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); + // Edge case 1 + c2 = new UptimeCalculator(); + dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59")); + assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); - // Edge case 2 - c2 = new UptimeCalculator(); - dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00")); - assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); + // Edge case 2 + c2 = new UptimeCalculator(); + dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00")); + assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); - // Test timezone - c2 = new UptimeCalculator(); - dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)")); - assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix()); -}); + // Test timezone + c2 = new UptimeCalculator(); + dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)")); + assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix()); + }); -test("Test lastDailyUptimeData", async (t) => { - let c2 = new UptimeCalculator(); - await c2.update(UP); - assert.strictEqual(c2.lastDailyUptimeData.up, 1); -}); + test("lastDailyUptimeData tracks UP status correctly", async () => { + let c2 = new UptimeCalculator(); + await c2.update(UP); + assert.strictEqual(c2.lastDailyUptimeData.up, 1); + }); -test("Test get24Hour Uptime and Avg Ping", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); + test("get24Hour() calculates uptime and average ping correctly", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); - // No data - let c2 = new UptimeCalculator(); - let data = c2.get24Hour(); - assert.strictEqual(data.uptime, 0); - assert.strictEqual(data.avgPing, null); + // No data + let c2 = new UptimeCalculator(); + let data = c2.get24Hour(); + assert.strictEqual(data.uptime, 0); + assert.strictEqual(data.avgPing, null); - // 1 Up - c2 = new UptimeCalculator(); - await c2.update(UP, 100); - let uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 1); - assert.strictEqual(c2.get24Hour().avgPing, 100); + // 1 Up + c2 = new UptimeCalculator(); + await c2.update(UP, 100); + let uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 1); + assert.strictEqual(c2.get24Hour().avgPing, 100); - // 2 Up - c2 = new UptimeCalculator(); - await c2.update(UP, 100); - await c2.update(UP, 200); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 1); - assert.strictEqual(c2.get24Hour().avgPing, 150); + // 2 Up + c2 = new UptimeCalculator(); + await c2.update(UP, 100); + await c2.update(UP, 200); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 1); + assert.strictEqual(c2.get24Hour().avgPing, 150); - // 3 Up - c2 = new UptimeCalculator(); - await c2.update(UP, 0); - await c2.update(UP, 100); - await c2.update(UP, 400); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 1); - assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666); + // 3 Up + c2 = new UptimeCalculator(); + await c2.update(UP, 0); + await c2.update(UP, 100); + await c2.update(UP, 400); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 1); + assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666); - // 1 MAINTENANCE - c2 = new UptimeCalculator(); - await c2.update(MAINTENANCE); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0); - assert.strictEqual(c2.get24Hour().avgPing, null); + // 1 MAINTENANCE + c2 = new UptimeCalculator(); + await c2.update(MAINTENANCE); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0); + assert.strictEqual(c2.get24Hour().avgPing, null); - // 1 PENDING - c2 = new UptimeCalculator(); - await c2.update(PENDING); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0); - assert.strictEqual(c2.get24Hour().avgPing, null); + // 1 PENDING + c2 = new UptimeCalculator(); + await c2.update(PENDING); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0); + assert.strictEqual(c2.get24Hour().avgPing, null); - // 1 DOWN - c2 = new UptimeCalculator(); - await c2.update(DOWN); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0); - assert.strictEqual(c2.get24Hour().avgPing, null); + // 1 DOWN + c2 = new UptimeCalculator(); + await c2.update(DOWN); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0); + assert.strictEqual(c2.get24Hour().avgPing, null); - // 2 DOWN - c2 = new UptimeCalculator(); - await c2.update(DOWN); - await c2.update(DOWN); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0); - assert.strictEqual(c2.get24Hour().avgPing, null); + // 2 DOWN + c2 = new UptimeCalculator(); + await c2.update(DOWN); + await c2.update(DOWN); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0); + assert.strictEqual(c2.get24Hour().avgPing, null); - // 1 DOWN, 1 UP - c2 = new UptimeCalculator(); - await c2.update(DOWN); - await c2.update(UP, 0.5); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0.5); - assert.strictEqual(c2.get24Hour().avgPing, 0.5); + // 1 DOWN, 1 UP + c2 = new UptimeCalculator(); + await c2.update(DOWN); + await c2.update(UP, 0.5); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0.5); + assert.strictEqual(c2.get24Hour().avgPing, 0.5); - // 1 UP, 1 DOWN - c2 = new UptimeCalculator(); - await c2.update(UP, 123); - await c2.update(DOWN); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0.5); - assert.strictEqual(c2.get24Hour().avgPing, 123); + // 1 UP, 1 DOWN + c2 = new UptimeCalculator(); + await c2.update(UP, 123); + await c2.update(DOWN); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0.5); + assert.strictEqual(c2.get24Hour().avgPing, 123); - // Add 24 hours - c2 = new UptimeCalculator(); - await c2.update(UP, 0); - await c2.update(UP, 0); - await c2.update(UP, 0); - await c2.update(UP, 1); - await c2.update(DOWN); - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0.8); - assert.strictEqual(c2.get24Hour().avgPing, 0.25); + // Add 24 hours + c2 = new UptimeCalculator(); + await c2.update(UP, 0); + await c2.update(UP, 0); + await c2.update(UP, 0); + await c2.update(UP, 1); + await c2.update(DOWN); + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0.8); + assert.strictEqual(c2.get24Hour().avgPing, 0.25); - UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); + UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); - // After 24 hours, even if there is no data, the uptime should be still 80% - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0.8); - assert.strictEqual(c2.get24Hour().avgPing, 0.25); + // After 24 hours, even if there is no data, the uptime should be still 80% + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0.8); + assert.strictEqual(c2.get24Hour().avgPing, 0.25); - // Add more 24 hours (48 hours) - UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); + // Add more 24 hours (48 hours) + UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); - // After 48 hours, even if there is no data, the uptime should be still 80% - uptime = c2.get24Hour().uptime; - assert.strictEqual(uptime, 0.8); - assert.strictEqual(c2.get24Hour().avgPing, 0.25); -}); + // After 48 hours, even if there is no data, the uptime should be still 80% + uptime = c2.get24Hour().uptime; + assert.strictEqual(uptime, 0.8); + assert.strictEqual(c2.get24Hour().avgPing, 0.25); + }); -test("Test get7DayUptime", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); + test("get7Day() calculates 7-day uptime correctly", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); - // No data - let c2 = new UptimeCalculator(); - let uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0); + // No data + let c2 = new UptimeCalculator(); + let uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0); - // 1 Up - c2 = new UptimeCalculator(); - await c2.update(UP); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 1); + // 1 Up + c2 = new UptimeCalculator(); + await c2.update(UP); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 1); - // 2 Up - c2 = new UptimeCalculator(); - await c2.update(UP); - await c2.update(UP); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 1); + // 2 Up + c2 = new UptimeCalculator(); + await c2.update(UP); + await c2.update(UP); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 1); - // 3 Up - c2 = new UptimeCalculator(); - await c2.update(UP); - await c2.update(UP); - await c2.update(UP); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 1); + // 3 Up + c2 = new UptimeCalculator(); + await c2.update(UP); + await c2.update(UP); + await c2.update(UP); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 1); - // 1 MAINTENANCE - c2 = new UptimeCalculator(); - await c2.update(MAINTENANCE); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0); + // 1 MAINTENANCE + c2 = new UptimeCalculator(); + await c2.update(MAINTENANCE); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0); - // 1 PENDING - c2 = new UptimeCalculator(); - await c2.update(PENDING); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0); + // 1 PENDING + c2 = new UptimeCalculator(); + await c2.update(PENDING); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0); - // 1 DOWN - c2 = new UptimeCalculator(); - await c2.update(DOWN); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0); + // 1 DOWN + c2 = new UptimeCalculator(); + await c2.update(DOWN); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0); - // 2 DOWN - c2 = new UptimeCalculator(); - await c2.update(DOWN); - await c2.update(DOWN); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0); + // 2 DOWN + c2 = new UptimeCalculator(); + await c2.update(DOWN); + await c2.update(DOWN); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0); - // 1 DOWN, 1 UP - c2 = new UptimeCalculator(); - await c2.update(DOWN); - await c2.update(UP); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0.5); + // 1 DOWN, 1 UP + c2 = new UptimeCalculator(); + await c2.update(DOWN); + await c2.update(UP); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0.5); - // 1 UP, 1 DOWN - c2 = new UptimeCalculator(); - await c2.update(UP); - await c2.update(DOWN); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0.5); + // 1 UP, 1 DOWN + c2 = new UptimeCalculator(); + await c2.update(UP); + await c2.update(DOWN); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0.5); - // Add 7 days - c2 = new UptimeCalculator(); - await c2.update(UP); - await c2.update(UP); - await c2.update(UP); - await c2.update(UP); - await c2.update(DOWN); - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0.8); - UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day"); + // Add 7 days + c2 = new UptimeCalculator(); + await c2.update(UP); + await c2.update(UP); + await c2.update(UP); + await c2.update(UP); + await c2.update(DOWN); + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0.8); + UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day"); - // After 7 days, even if there is no data, the uptime should be still 80% - uptime = c2.get7Day().uptime; - assert.strictEqual(uptime, 0.8); + // After 7 days, even if there is no data, the uptime should be still 80% + uptime = c2.get7Day().uptime; + assert.strictEqual(uptime, 0.8); + }); -}); + test("get30Day() calculates 30-day uptime correctly with 1 check per day", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); -test("Test get30DayUptime (1 check per day)", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); + let c2 = new UptimeCalculator(); + let uptime = c2.get30Day().uptime; + assert.strictEqual(uptime, 0); - let c2 = new UptimeCalculator(); - let uptime = c2.get30Day().uptime; - assert.strictEqual(uptime, 0); + let up = 0; + let down = 0; + let flip = true; + for (let i = 0; i < 30; i++) { + UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day"); - let up = 0; - let down = 0; - let flip = true; - for (let i = 0; i < 30; i++) { - UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day"); + if (flip) { + await c2.update(UP); + up++; + } else { + await c2.update(DOWN); + down++; + } - if (flip) { - await c2.update(UP); - up++; - } else { - await c2.update(DOWN); - down++; + uptime = c2.get30Day().uptime; + assert.strictEqual(uptime, up / (up + down)); + + flip = !flip; } - uptime = c2.get30Day().uptime; - assert.strictEqual(uptime, up / (up + down)); + // Last 7 days + // Down, Up, Down, Up, Down, Up, Down + // So 3 UP + assert.strictEqual(c2.get7Day().uptime, 3 / 7); + }); - flip = !flip; - } + test("get1Year() calculates 1-year uptime correctly with 1 check per day", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); - // Last 7 days - // Down, Up, Down, Up, Down, Up, Down - // So 3 UP - assert.strictEqual(c2.get7Day().uptime, 3 / 7); -}); + let c2 = new UptimeCalculator(); + let uptime = c2.get1Year().uptime; + assert.strictEqual(uptime, 0); -test("Test get1YearUptime (1 check per day)", async (t) => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); + let flip = true; + for (let i = 0; i < 365; i++) { + UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day"); - let c2 = new UptimeCalculator(); - let uptime = c2.get1Year().uptime; - assert.strictEqual(uptime, 0); + if (flip) { + await c2.update(UP); + } else { + await c2.update(DOWN); + } - let flip = true; - for (let i = 0; i < 365; i++) { - UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day"); - - if (flip) { - await c2.update(UP); - } else { - await c2.update(DOWN); + uptime = c2.get30Day().time; + flip = !flip; } - uptime = c2.get30Day().time; - flip = !flip; - } + assert.strictEqual(c2.get1Year().uptime, 183 / 365); + assert.strictEqual(c2.get30Day().uptime, 15 / 30); + assert.strictEqual(c2.get7Day().uptime, 4 / 7); + }); - assert.strictEqual(c2.get1Year().uptime, 183 / 365); - assert.strictEqual(c2.get30Day().uptime, 15 / 30); - assert.strictEqual(c2.get7Day().uptime, 4 / 7); + describe("Worst case scenario", () => { + test("handles year-long simulation with various statuses", { + skip: process.env.GITHUB_ACTIONS // Not stable on GitHub Actions" + }, async (t) => { + console.log("Memory usage before preparation", memoryUsage()); + + let c = new UptimeCalculator(); + let up = 0; + let down = 0; + let interval = 20; + + await t.test("Prepare data", async () => { + UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); + + // Since 2023-08-12 will be out of 365 range, it starts from 2023-08-13 actually + let actualStartDate = dayjs.utc("2023-08-13 00:00:00").unix(); + + // Simulate 1s interval for a year + for (let i = 0; i < 365 * 24 * 60 * 60; i += interval) { + UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(interval, "second"); + + //Randomly UP, DOWN, MAINTENANCE, PENDING + let rand = Math.random(); + if (rand < 0.25) { + c.update(UP); + if (UptimeCalculator.currentDate.unix() > actualStartDate) { + up++; + } + } else if (rand < 0.5) { + c.update(DOWN); + if (UptimeCalculator.currentDate.unix() > actualStartDate) { + down++; + } + } else if (rand < 0.75) { + c.update(MAINTENANCE); + if (UptimeCalculator.currentDate.unix() > actualStartDate) { + //up++; + } + } else { + c.update(PENDING); + if (UptimeCalculator.currentDate.unix() > actualStartDate) { + down++; + } + } + } + console.log("Final Date: ", UptimeCalculator.currentDate.format("YYYY-MM-DD HH:mm:ss")); + console.log("Memory usage before preparation", memoryUsage()); + + assert.strictEqual(c.minutelyUptimeDataList.length(), 1440); + assert.strictEqual(c.dailyUptimeDataList.length(), 365); + }); + + await t.test("get1YearUptime()", async () => { + assert.strictEqual(c.get1Year().uptime, up / (up + down)); + }); + }); + }); }); /** @@ -362,64 +421,3 @@ function memoryUsage() { external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`, }; } - -test("Worst case", async (t) => { - - // Disable on GitHub Actions, as it is not stable on it - if (process.env.GITHUB_ACTIONS) { - return; - } - - console.log("Memory usage before preparation", memoryUsage()); - - let c = new UptimeCalculator(); - let up = 0; - let down = 0; - let interval = 20; - - await t.test("Prepare data", async () => { - UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); - - // Since 2023-08-12 will be out of 365 range, it starts from 2023-08-13 actually - let actualStartDate = dayjs.utc("2023-08-13 00:00:00").unix(); - - // Simulate 1s interval for a year - for (let i = 0; i < 365 * 24 * 60 * 60; i += interval) { - UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(interval, "second"); - - //Randomly UP, DOWN, MAINTENANCE, PENDING - let rand = Math.random(); - if (rand < 0.25) { - c.update(UP); - if (UptimeCalculator.currentDate.unix() > actualStartDate) { - up++; - } - } else if (rand < 0.5) { - c.update(DOWN); - if (UptimeCalculator.currentDate.unix() > actualStartDate) { - down++; - } - } else if (rand < 0.75) { - c.update(MAINTENANCE); - if (UptimeCalculator.currentDate.unix() > actualStartDate) { - //up++; - } - } else { - c.update(PENDING); - if (UptimeCalculator.currentDate.unix() > actualStartDate) { - down++; - } - } - } - console.log("Final Date: ", UptimeCalculator.currentDate.format("YYYY-MM-DD HH:mm:ss")); - console.log("Memory usage before preparation", memoryUsage()); - - assert.strictEqual(c.minutelyUptimeDataList.length(), 1440); - assert.strictEqual(c.dailyUptimeDataList.length(), 365); - }); - - await t.test("get1YearUptime()", async () => { - assert.strictEqual(c.get1Year().uptime, up / (up + down)); - }); - -}); From b91b73a4eb155eaaf8cfeeae23340e08e0cc3780 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:27:37 +0100 Subject: [PATCH 09/35] move monitoring tests to better folder --- test/backend-test/{ => monitors}/test-grpc.js | 0 test/backend-test/{ => monitors}/test-mqtt.js | 0 test/backend-test/{ => monitors}/test-mssql.js | 0 test/backend-test/{ => monitors}/test-postgres.js | 0 test/backend-test/{ => monitors}/test-rabbitmq.js | 0 test/backend-test/{ => monitors}/test-tcp.js | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename test/backend-test/{ => monitors}/test-grpc.js (100%) rename test/backend-test/{ => monitors}/test-mqtt.js (100%) rename test/backend-test/{ => monitors}/test-mssql.js (100%) rename test/backend-test/{ => monitors}/test-postgres.js (100%) rename test/backend-test/{ => monitors}/test-rabbitmq.js (100%) rename test/backend-test/{ => monitors}/test-tcp.js (100%) diff --git a/test/backend-test/test-grpc.js b/test/backend-test/monitors/test-grpc.js similarity index 100% rename from test/backend-test/test-grpc.js rename to test/backend-test/monitors/test-grpc.js diff --git a/test/backend-test/test-mqtt.js b/test/backend-test/monitors/test-mqtt.js similarity index 100% rename from test/backend-test/test-mqtt.js rename to test/backend-test/monitors/test-mqtt.js diff --git a/test/backend-test/test-mssql.js b/test/backend-test/monitors/test-mssql.js similarity index 100% rename from test/backend-test/test-mssql.js rename to test/backend-test/monitors/test-mssql.js diff --git a/test/backend-test/test-postgres.js b/test/backend-test/monitors/test-postgres.js similarity index 100% rename from test/backend-test/test-postgres.js rename to test/backend-test/monitors/test-postgres.js diff --git a/test/backend-test/test-rabbitmq.js b/test/backend-test/monitors/test-rabbitmq.js similarity index 100% rename from test/backend-test/test-rabbitmq.js rename to test/backend-test/monitors/test-rabbitmq.js diff --git a/test/backend-test/test-tcp.js b/test/backend-test/monitors/test-tcp.js similarity index 100% rename from test/backend-test/test-tcp.js rename to test/backend-test/monitors/test-tcp.js From 9abded0baf3cf359cb80080b83ca9e8efbb97bcf Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:34:05 +0100 Subject: [PATCH 10/35] fix imports --- server/model/domain_expiry.js | 4 ++-- test/backend-test/monitors/test-grpc.js | 4 ++-- test/backend-test/monitors/test-mqtt.js | 4 ++-- test/backend-test/monitors/test-mssql.js | 4 ++-- test/backend-test/monitors/test-postgres.js | 4 ++-- test/backend-test/monitors/test-rabbitmq.js | 4 ++-- test/backend-test/monitors/test-tcp.js | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/server/model/domain_expiry.js b/server/model/domain_expiry.js index b796a81d1..d365c03ab 100644 --- a/server/model/domain_expiry.js +++ b/server/model/domain_expiry.js @@ -172,9 +172,9 @@ class DomainExpiry extends BeanModel { } /** - * @returns {(Date|null)} Expiry date from RDAP + * @returns {Promise<(Date|null)>} Expiry date from RDAP */ - getExpiryDate() { + async getExpiryDate() { return getRdapDomainExpiryDate(this.domain); } diff --git a/test/backend-test/monitors/test-grpc.js b/test/backend-test/monitors/test-grpc.js index 31b588cff..24edfb81f 100644 --- a/test/backend-test/monitors/test-grpc.js +++ b/test/backend-test/monitors/test-grpc.js @@ -2,8 +2,8 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); const grpc = require("@grpc/grpc-js"); const protoLoader = require("@grpc/proto-loader"); -const { GrpcKeywordMonitorType } = require("../../server/monitor-types/grpc"); -const { UP, PENDING } = require("../../src/util"); +const { GrpcKeywordMonitorType } = require("../../../server/monitor-types/grpc"); +const { UP, PENDING } = require("../../../src/util"); const fs = require("fs"); const path = require("path"); const os = require("os"); diff --git a/test/backend-test/monitors/test-mqtt.js b/test/backend-test/monitors/test-mqtt.js index 921df48fc..dbcf56a83 100644 --- a/test/backend-test/monitors/test-mqtt.js +++ b/test/backend-test/monitors/test-mqtt.js @@ -2,8 +2,8 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); const { HiveMQContainer } = require("@testcontainers/hivemq"); const mqtt = require("mqtt"); -const { MqttMonitorType } = require("../../server/monitor-types/mqtt"); -const { UP, PENDING } = require("../../src/util"); +const { MqttMonitorType } = require("../../../server/monitor-types/mqtt"); +const { UP, PENDING } = require("../../../src/util"); /** * Runs an MQTT test with the diff --git a/test/backend-test/monitors/test-mssql.js b/test/backend-test/monitors/test-mssql.js index afdd1faf8..091f01acc 100644 --- a/test/backend-test/monitors/test-mssql.js +++ b/test/backend-test/monitors/test-mssql.js @@ -1,8 +1,8 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); const { MSSQLServerContainer } = require("@testcontainers/mssqlserver"); -const { MssqlMonitorType } = require("../../server/monitor-types/mssql"); -const { UP, PENDING } = require("../../src/util"); +const { MssqlMonitorType } = require("../../../server/monitor-types/mssql"); +const { UP, PENDING } = require("../../../src/util"); /** * Helper function to create and start a MSSQL container diff --git a/test/backend-test/monitors/test-postgres.js b/test/backend-test/monitors/test-postgres.js index 71d26d3a1..505e0fc90 100644 --- a/test/backend-test/monitors/test-postgres.js +++ b/test/backend-test/monitors/test-postgres.js @@ -1,8 +1,8 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); const { PostgreSqlContainer } = require("@testcontainers/postgresql"); -const { PostgresMonitorType } = require("../../server/monitor-types/postgres"); -const { UP, PENDING } = require("../../src/util"); +const { PostgresMonitorType } = require("../../../server/monitor-types/postgres"); +const { UP, PENDING } = require("../../../src/util"); describe( "Postgres Single Node", diff --git a/test/backend-test/monitors/test-rabbitmq.js b/test/backend-test/monitors/test-rabbitmq.js index 31f018aa9..88a04afa1 100644 --- a/test/backend-test/monitors/test-rabbitmq.js +++ b/test/backend-test/monitors/test-rabbitmq.js @@ -1,8 +1,8 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); const { RabbitMQContainer } = require("@testcontainers/rabbitmq"); -const { RabbitMqMonitorType } = require("../../server/monitor-types/rabbitmq"); -const { UP, PENDING } = require("../../src/util"); +const { RabbitMqMonitorType } = require("../../../server/monitor-types/rabbitmq"); +const { UP, PENDING } = require("../../../src/util"); describe("RabbitMQ Single Node", { skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), diff --git a/test/backend-test/monitors/test-tcp.js b/test/backend-test/monitors/test-tcp.js index 98f168c24..71f0f2f87 100644 --- a/test/backend-test/monitors/test-tcp.js +++ b/test/backend-test/monitors/test-tcp.js @@ -1,7 +1,7 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); -const { TCPMonitorType } = require("../../server/monitor-types/tcp"); -const { UP, PENDING } = require("../../src/util"); +const { TCPMonitorType } = require("../../../server/monitor-types/tcp"); +const { UP, PENDING } = require("../../../src/util"); const net = require("net"); /** From 49eb0a34a10b6789ec1e495056cc4bb7496976f6 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:34:31 +0100 Subject: [PATCH 11/35] migrate domains to use test suites --- test/backend-test/test-domain.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/test/backend-test/test-domain.js b/test/backend-test/test-domain.js index 535029531..d254244bf 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 test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const DomainExpiry = require("../../server/model/domain_expiry"); const mockWebhook = require("../mock-webhook"); @@ -12,30 +12,34 @@ const { setSetting } = require("../../server/util-server"); const testDb = new TestDB(); -test("Domain Expiry", async (t) => { - await testDb.create(); - Notification.init(); - +describe("Domain Expiry", () => { const monHttpCom = { type: "http", url: "https://www.google.com", domainExpiryNotification: true }; - await t.test("Should get expiry date for .wiki with no A record", async () => { + + test("getExpiryDate() returns correct expiry date for .wiki domain with no A record", async () => { + await testDb.create(); + Notification.init(); + const d = DomainExpiry.createByName("google.wiki"); assert.deepEqual(await d.getExpiryDate(), new Date("2026-11-26T23:59:59.000Z")); }); - await t.test("Should get expiration date for .com from RDAP", async () => { + + test("forMonitor() retrieves expiration date for .com domain from RDAP", async () => { const domain = await DomainExpiry.forMonitor(monHttpCom); const expiryFromRdap = await domain.getExpiryDate(); // from RDAP assert.deepEqual(expiryFromRdap, new Date("2028-09-14T04:00:00.000Z")); }); - await t.test("Should have expiration date cached in database", async () => { + + test("checkExpiry() caches expiration date in database", async () => { await DomainExpiry.checkExpiry(monHttpCom); // RDAP -> Cache const domain = await DomainExpiry.findByName("google.com"); assert(Date.now() - domain.lastCheck < 5 * 1000); }); - await t.test("Should trigger notify for expiring domain", async () => { + + test("sendNotifications() triggers notification for expiring domain", async () => { await DomainExpiry.findByName("google.com"); const hook = { "port": 3010, @@ -60,10 +64,10 @@ test("Domain Expiry", async (t) => { mockWebhook(hook.port, hook.url) ]); assert.match(data.msg, /will expire in/); + + setTimeout(async () => { + Settings.stopCacheCleaner(); + await testDb.destroy(); + }, 200); }); -}).finally(() => { - setTimeout(async () => { - Settings.stopCacheCleaner(); - await testDb.destroy(); - }, 200); }); From afef0c0e61e3830f9153f93113ac182b4eddd28c Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:35:26 +0100 Subject: [PATCH 12/35] move websokcet --- test/backend-test/{ => monitors}/test-websocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename test/backend-test/{ => monitors}/test-websocket.js (98%) diff --git a/test/backend-test/test-websocket.js b/test/backend-test/monitors/test-websocket.js similarity index 98% rename from test/backend-test/test-websocket.js rename to test/backend-test/monitors/test-websocket.js index 3eeeb3243..545cad466 100644 --- a/test/backend-test/test-websocket.js +++ b/test/backend-test/monitors/test-websocket.js @@ -1,8 +1,8 @@ const { WebSocketServer } = require("ws"); const { describe, test } = require("node:test"); const assert = require("node:assert"); -const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade"); -const { UP, PENDING } = require("../../src/util"); +const { WebSocketMonitorType } = require("../../../server/monitor-types/websocket-upgrade"); +const { UP, PENDING } = require("../../../src/util"); const net = require("node:net"); /** From 625456ec76b554f4473e404227bb9c78341f1fca Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:37:03 +0100 Subject: [PATCH 13/35] migrate maintenance to use suites --- test/backend-test/test-maintenance.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/backend-test/test-maintenance.js b/test/backend-test/test-maintenance.js index a23bd77bb..bb8304d93 100644 --- a/test/backend-test/test-maintenance.js +++ b/test/backend-test/test-maintenance.js @@ -1,4 +1,4 @@ -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const dayjs = require("dayjs"); const { SQL_DATETIME_FORMAT } = require("../../src/util"); @@ -11,20 +11,19 @@ dayjs.extend(require("dayjs/plugin/customParseFormat")); * Issue: MariaDB rejects ISO format dates like '2025-12-19T01:04:02.129Z' * Fix: Use SQL_DATETIME_FORMAT ('YYYY-MM-DD HH:mm:ss') instead of toISOString() */ -test("Maintenance Date Format - MariaDB Compatibility", async (t) => { - - await t.test("SQL_DATETIME_FORMAT constant should match MariaDB format", async () => { +describe("Maintenance Date Format - MariaDB Compatibility", () => { + test("SQL_DATETIME_FORMAT constant should match MariaDB format", () => { assert.strictEqual(SQL_DATETIME_FORMAT, "YYYY-MM-DD HH:mm:ss"); }); - await t.test("Format date using SQL_DATETIME_FORMAT", async () => { + test("format() produces SQL-compatible datetime string", () => { const current = dayjs.utc("2025-12-19T01:04:02.129Z"); const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); assert.strictEqual(sqlFormat, "2025-12-19 01:04:02"); - }); + });move websokcet - await t.test("SQL format should not contain ISO markers (T, Z)", async () => { + test("SQL format does not contain ISO markers (T, Z)", () => { const current = dayjs.utc("2025-12-19T01:04:02.129Z"); const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); @@ -32,7 +31,7 @@ test("Maintenance Date Format - MariaDB Compatibility", async (t) => { assert.strictEqual(sqlFormat.includes("Z"), false, "SQL format should not contain 'Z'"); }); - await t.test("SQL format should match YYYY-MM-DD HH:mm:ss pattern", async () => { + test("SQL format matches YYYY-MM-DD HH:mm:ss pattern", () => { const current = dayjs.utc("2025-12-19T01:04:02.129Z"); const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); const sqlDateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; @@ -40,7 +39,7 @@ test("Maintenance Date Format - MariaDB Compatibility", async (t) => { assert.strictEqual(sqlDateTimeRegex.test(sqlFormat), true); }); - await t.test("Parse SQL datetime back to dayjs preserves timestamp", async () => { + test("parsing SQL datetime back to dayjs preserves timestamp", () => { const originalDate = dayjs.utc("2025-12-19T01:04:02.000Z"); const sqlFormat = originalDate.utc().format(SQL_DATETIME_FORMAT); const parsedDate = dayjs.utc(sqlFormat, SQL_DATETIME_FORMAT); @@ -48,18 +47,17 @@ test("Maintenance Date Format - MariaDB Compatibility", async (t) => { assert.strictEqual(parsedDate.unix(), originalDate.unix()); }); - await t.test("Edge case: midnight timestamp", async () => { + test("formats midnight timestamp correctly", () => { const midnight = dayjs.utc("2025-01-01T00:00:00.000Z"); const sqlFormat = midnight.utc().format(SQL_DATETIME_FORMAT); assert.strictEqual(sqlFormat, "2025-01-01 00:00:00"); }); - await t.test("Edge case: end of day timestamp", async () => { + test("formats end of day timestamp correctly", () => { const endOfDay = dayjs.utc("2025-12-31T23:59:59.999Z"); const sqlFormat = endOfDay.utc().format(SQL_DATETIME_FORMAT); assert.strictEqual(sqlFormat, "2025-12-31 23:59:59"); }); - }); From 19880f479e22b0f6abf10d5c55742d4e762f3fb3 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:39:00 +0100 Subject: [PATCH 14/35] cert hostname --- test/backend-test/test-cert-hostname-match.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/backend-test/test-cert-hostname-match.js b/test/backend-test/test-cert-hostname-match.js index fb7c822ed..f35b7aa76 100644 --- a/test/backend-test/test-cert-hostname-match.js +++ b/test/backend-test/test-cert-hostname-match.js @@ -1,4 +1,4 @@ -const { test } = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); @@ -36,12 +36,14 @@ iPenGDCg1awOyRnvxNq1MtMDkR9AHwksukzwiYNexYjyvE2t0UzXhFXwazQ3 -----END CERTIFICATE----- `; -test("Certificate and hostname match", () => { - const result = checkCertificateHostname(testCert, "www.eff.org"); - assert.strictEqual(result, true); -}); +describe("Certificate Hostname Validation", () => { + test("checkCertificateHostname() returns true when certificate matches hostname", () => { + const result = checkCertificateHostname(testCert, "www.eff.org"); + assert.strictEqual(result, true); + }); -test("Certificate and hostname mismatch", () => { - const result = checkCertificateHostname(testCert, "example.com"); - assert.strictEqual(result, false); + test("checkCertificateHostname() returns false when certificate does not match hostname", () => { + const result = checkCertificateHostname(testCert, "example.com"); + assert.strictEqual(result, false); + }); }); From 453e468b77a0ab24861dbf8aacc4fe9a967892fc Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:42:56 +0100 Subject: [PATCH 15/35] refactor the system utilities test --- test/backend-test/test-util.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test/backend-test/test-util.js b/test/backend-test/test-util.js index 470113329..63660d4fb 100644 --- a/test/backend-test/test-util.js +++ b/test/backend-test/test-util.js @@ -1,18 +1,26 @@ -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); const { getDaysRemaining, getDaysBetween } = require("../../server/util-server"); -test("Test getDaysBetween", async (t) => { - let days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10)); - assert.strictEqual(days, 3); - days = getDaysBetween(new Date(2024, 9, 7), new Date(2025, 9, 10)); - assert.strictEqual(days, 368); -}); +describe("Server Utilities", () => { + test("getDaysBetween() calculates days between dates within same month", () => { + const days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10)); + assert.strictEqual(days, 3); + }); -test("Test getDaysRemaining", async (t) => { - let days = getDaysRemaining(new Date(2025, 9, 7), new Date(2025, 9, 10)); - assert.strictEqual(days, 3); - days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7)); - assert.strictEqual(days, -3); + test("getDaysBetween() calculates days between dates across years", () => { + const days = getDaysBetween(new Date(2024, 9, 7), new Date(2025, 9, 10)); + assert.strictEqual(days, 368); + }); + + test("getDaysRemaining() returns positive value when target date is in future", () => { + const days = getDaysRemaining(new Date(2025, 9, 7), new Date(2025, 9, 10)); + assert.strictEqual(days, 3); + }); + + test("getDaysRemaining() returns negative value when target date is in past", () => { + const days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7)); + assert.strictEqual(days, -3); + }); }); From ac1c43d2ccecddc67d5eb9ec152edc3059dcb405 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:47:31 +0100 Subject: [PATCH 16/35] move the webhook to a better place --- .../notification-providers}/mock-webhook.js | 56 +++++++++---------- test/backend-test/test-domain.js | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) rename test/{ => backend-test/notification-providers}/mock-webhook.js (96%) diff --git a/test/mock-webhook.js b/test/backend-test/notification-providers/mock-webhook.js similarity index 96% rename from test/mock-webhook.js rename to test/backend-test/notification-providers/mock-webhook.js index 23bf192c7..70cc5fdf9 100644 --- a/test/mock-webhook.js +++ b/test/backend-test/notification-providers/mock-webhook.js @@ -1,28 +1,28 @@ -const express = require("express"); -const bodyParser = require("body-parser"); - -/** - * @param {number} port Port number - * @param {string} url Webhook URL - * @param {number} timeout Timeout - * @returns {Promise} Webhook data - */ -async function mockWebhook(port, url, timeout = 2500) { - return new Promise((resolve, reject) => { - const app = express(); - const tmo = setTimeout(() => { - server.close(); - reject({ reason: "Timeout" }); - }, timeout); - app.use(bodyParser.json()); // Middleware to parse JSON bodies - app.post(`/${url}`, (req, res) => { - res.status(200).send("OK"); - server.close(); - tmo && clearTimeout(tmo); - resolve(req.body); - }); - const server = app.listen(port); - }); -} - -module.exports = mockWebhook; +const express = require("express"); +const bodyParser = require("body-parser"); + +/** + * @param {number} port Port number + * @param {string} url Webhook URL + * @param {number} timeout Timeout + * @returns {Promise} Webhook data + */ +async function mockWebhook(port, url, timeout = 2500) { + return new Promise((resolve, reject) => { + const app = express(); + const tmo = setTimeout(() => { + server.close(); + reject({ reason: "Timeout" }); + }, timeout); + app.use(bodyParser.json()); // Middleware to parse JSON bodies + app.post(`/${url}`, (req, res) => { + res.status(200).send("OK"); + server.close(); + tmo && clearTimeout(tmo); + resolve(req.body); + }); + const server = app.listen(port); + }); +} + +module.exports = mockWebhook; diff --git a/test/backend-test/test-domain.js b/test/backend-test/test-domain.js index d254244bf..451cdfdc9 100644 --- a/test/backend-test/test-domain.js +++ b/test/backend-test/test-domain.js @@ -3,7 +3,7 @@ process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(","); const { describe, test } = require("node:test"); const assert = require("node:assert"); const DomainExpiry = require("../../server/model/domain_expiry"); -const mockWebhook = require("../mock-webhook"); +const mockWebhook = require("./notification-providers/mock-webhook"); const TestDB = require("../mock-testdb"); const { R } = require("redbean-node"); const { Notification } = require("../../server/notification"); From d493291025432f01cb69036deea9c63bc0779449 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:51:02 +0100 Subject: [PATCH 17/35] update the readme --- test/backend-test/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/backend-test/README.md b/test/backend-test/README.md index 775ffb7a8..8822562ab 100644 --- a/test/backend-test/README.md +++ b/test/backend-test/README.md @@ -4,14 +4,26 @@ Documentation: https://nodejs.org/api/test.html Create a test file in this directory with the name `*.js`. +> [!TIP] +> Writing great tests is hard. +> +> You can make our live much simpler by following this guidance: +> - Use `describe()` to group related tests +> - Use `test()` for individual test cases +> - One test per scenario +> - Use descriptive test names: `function() [behavior] [condition]` +> - Don't prefix with "Test" or "Should" + ## Template ```js -const test = require("node:test"); +const { describe, test } = require("node:test"); const assert = require("node:assert"); -test("Test name", async (t) => { - assert.strictEqual(1, 1); +describe("Feature Name", () => { + test("function() returns expected value when condition is met", () => { + assert.strictEqual(1, 1); + }); }); ``` From a27b4eb93f0e06233c05079feb2c0d5ab2eb7b52 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 16:54:34 +0100 Subject: [PATCH 18/35] fix fmt --- test/backend-test/monitor-conditions/test-evaluator.js | 1 - test/backend-test/test-maintenance.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/backend-test/monitor-conditions/test-evaluator.js b/test/backend-test/monitor-conditions/test-evaluator.js index f2e967b81..c731a6792 100644 --- a/test/backend-test/monitor-conditions/test-evaluator.js +++ b/test/backend-test/monitor-conditions/test-evaluator.js @@ -1,4 +1,3 @@ - const { describe, test } = require("node:test"); const assert = require("node:assert"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); diff --git a/test/backend-test/test-maintenance.js b/test/backend-test/test-maintenance.js index bb8304d93..27f3ff6b9 100644 --- a/test/backend-test/test-maintenance.js +++ b/test/backend-test/test-maintenance.js @@ -21,7 +21,7 @@ describe("Maintenance Date Format - MariaDB Compatibility", () => { const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); assert.strictEqual(sqlFormat, "2025-12-19 01:04:02"); - });move websokcet + }); test("SQL format does not contain ISO markers (T, Z)", () => { const current = dayjs.utc("2025-12-19T01:04:02.129Z"); From f426e5819ac99a5cf0670ba5db440dd99bbd0619 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 17:01:32 +0100 Subject: [PATCH 19/35] reword more of our testsuite names --- test/backend-test/monitors/test-grpc.js | 12 +++---- test/backend-test/monitors/test-mqtt.js | 30 ++++++++-------- test/backend-test/monitors/test-mssql.js | 16 ++++----- test/backend-test/monitors/test-postgres.js | 4 +-- test/backend-test/monitors/test-rabbitmq.js | 4 +-- test/backend-test/monitors/test-tcp.js | 36 ++++---------------- test/backend-test/monitors/test-websocket.js | 28 +++++++-------- test/backend-test/test-migration.js | 2 +- 8 files changed, 55 insertions(+), 77 deletions(-) diff --git a/test/backend-test/monitors/test-grpc.js b/test/backend-test/monitors/test-grpc.js index 24edfb81f..def839044 100644 --- a/test/backend-test/monitors/test-grpc.js +++ b/test/backend-test/monitors/test-grpc.js @@ -82,7 +82,7 @@ async function createTestGrpcServer(port, methodHandlers) { describe("GrpcKeywordMonitorType", { skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), }, () => { - test("gRPC keyword found in response", async () => { + test("check() sets status to UP when keyword is found in response", async () => { const port = 50051; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { @@ -118,7 +118,7 @@ describe("GrpcKeywordMonitorType", { } }); - test("gRPC keyword not found in response", async () => { + test("check() rejects when keyword is not found in response", async () => { const port = 50052; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { @@ -158,7 +158,7 @@ describe("GrpcKeywordMonitorType", { } }); - test("gRPC inverted keyword - keyword present (should fail)", async () => { + test("check() rejects when inverted keyword is present in response", async () => { const port = 50053; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { @@ -198,7 +198,7 @@ describe("GrpcKeywordMonitorType", { } }); - test("gRPC inverted keyword - keyword not present (should pass)", async () => { + test("check() sets status to UP when inverted keyword is not present in response", async () => { const port = 50054; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { @@ -234,7 +234,7 @@ describe("GrpcKeywordMonitorType", { } }); - test("gRPC connection failure", async () => { + test("check() rejects when gRPC server is unreachable", async () => { const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: "localhost:50099", @@ -262,7 +262,7 @@ describe("GrpcKeywordMonitorType", { ); }); - test("gRPC response truncation for long messages", async () => { + test("check() truncates long response messages in error output", async () => { const port = 50055; const longMessage = "A".repeat(100) + " with SUCCESS keyword"; diff --git a/test/backend-test/monitors/test-mqtt.js b/test/backend-test/monitors/test-mqtt.js index dbcf56a83..a361e868e 100644 --- a/test/backend-test/monitors/test-mqtt.js +++ b/test/backend-test/monitors/test-mqtt.js @@ -58,91 +58,91 @@ describe("MqttMonitorType", { concurrency: 4, skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64") }, () => { - test("valid keywords (type=default)", async () => { + test("check() sets status to UP when keyword is found in message (type=default)", async () => { const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); }); - test("valid nested topic", async () => { + test("check() sets status to UP when keyword is found in 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 nested topic (with special chars)", async () => { + test("check() sets status to UP when keyword is found in nested topic with special characters", async () => { const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/'/$/./*/%", "a/'/$/./*/%"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Topic: a/'/$/./*/%; Message: -> KEYWORD <-"); }); - test("valid wildcard topic (with #)", async () => { + test("check() sets status to UP when keyword is found using # wildcard", 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 () => { + test("check() sets status to UP when keyword is found using + wildcard", 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("valid wildcard topic (with + and #)", async () => { + test("check() sets status to UP when keyword is found using + and # wildcards", async () => { const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c/#", "a/b/c/d/e"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Topic: a/b/c/d/e; Message: -> KEYWORD <-"); }); - test("invalid topic", async () => { + test("check() rejects with timeout when topic does not match", 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 () => { + test("check() rejects with timeout when # wildcard is not last character", 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 () => { + test("check() rejects with timeout when + wildcard topic does not match", 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("check() sets status to UP when keyword is found in message (type=keyword)", async () => { const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); }); - test("invalid keywords (type=default)", async () => { + test("check() rejects when keyword is not found in message (type=default)", async () => { await assert.rejects( testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"), new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"), ); }); - test("invalid keyword (type=keyword)", async () => { + test("check() rejects when keyword is not found in message (type=keyword)", async () => { await assert.rejects( testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"), new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"), ); }); - test("valid json-query", async () => { + test("check() sets status to UP when json-query finds expected value", async () => { // works because the monitors' jsonPath is hard-coded to "firstProp" const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}"); assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.msg, "Message received, expected value is found"); }); - test("invalid (because query fails) json-query", async () => { + test("check() rejects when json-query path returns undefined", async () => { // works because the monitors' jsonPath is hard-coded to "firstProp" await assert.rejects( testMqtt("[not_relevant]", "json-query", "{}"), @@ -150,7 +150,7 @@ describe("MqttMonitorType", { ); }); - test("invalid (because successMessage fails) json-query", async () => { + test("check() rejects when json-query value does not match expected value", async () => { // works because the monitors' jsonPath is hard-coded to "firstProp" await assert.rejects( testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"), diff --git a/test/backend-test/monitors/test-mssql.js b/test/backend-test/monitors/test-mssql.js index 091f01acc..f265bcdff 100644 --- a/test/backend-test/monitors/test-mssql.js +++ b/test/backend-test/monitors/test-mssql.js @@ -26,7 +26,7 @@ describe( (process.platform !== "linux" || process.arch !== "x64"), }, () => { - test("MSSQL is running", async () => { + test("check() sets status to UP when MSSQL server is reachable", async () => { let mssqlContainer; try { @@ -69,7 +69,7 @@ describe( } }); - test("MSSQL with custom query returning single value", async () => { + test("check() sets status to UP when custom query returns single value", async () => { const mssqlContainer = await createAndStartMSSQLContainer(); const mssqlMonitor = new MssqlMonitorType(); @@ -97,7 +97,7 @@ describe( } }); - test("MSSQL with custom query and condition that passes", async () => { + test("check() sets status to UP when custom query result meets condition", async () => { const mssqlContainer = await createAndStartMSSQLContainer(); const mssqlMonitor = new MssqlMonitorType(); @@ -133,7 +133,7 @@ describe( } }); - test("MSSQL with custom query and condition that fails", async () => { + test("check() rejects when custom query result does not meet condition", async () => { const mssqlContainer = await createAndStartMSSQLContainer(); const mssqlMonitor = new MssqlMonitorType(); @@ -174,7 +174,7 @@ describe( } }); - test("MSSQL query returns no results", async () => { + test("check() rejects when query returns no results", async () => { const mssqlContainer = await createAndStartMSSQLContainer(); const mssqlMonitor = new MssqlMonitorType(); @@ -207,7 +207,7 @@ describe( } }); - test("MSSQL query returns multiple rows", async () => { + test("check() rejects when query returns multiple rows", async () => { const mssqlContainer = await createAndStartMSSQLContainer(); const mssqlMonitor = new MssqlMonitorType(); @@ -240,7 +240,7 @@ describe( } }); - test("MSSQL query returns multiple columns", async () => { + test("check() rejects when query returns multiple columns", async () => { const mssqlContainer = await createAndStartMSSQLContainer(); const mssqlMonitor = new MssqlMonitorType(); @@ -273,7 +273,7 @@ describe( } }); - test("MSSQL is not running", async () => { + test("check() rejects when MSSQL server is not reachable", async () => { const mssqlMonitor = new MssqlMonitorType(); const monitor = { databaseConnectionString: diff --git a/test/backend-test/monitors/test-postgres.js b/test/backend-test/monitors/test-postgres.js index 505e0fc90..098c862a1 100644 --- a/test/backend-test/monitors/test-postgres.js +++ b/test/backend-test/monitors/test-postgres.js @@ -12,7 +12,7 @@ describe( (process.platform !== "linux" || process.arch !== "x64"), }, () => { - test("Postgres is running", async () => { + test("check() sets status to UP when Postgres server is reachable", async () => { // The default timeout of 30 seconds might not be enough for the container to start const postgresContainer = await new PostgreSqlContainer( "postgres:latest" @@ -37,7 +37,7 @@ describe( } }); - test("Postgres is not running", async () => { + test("check() rejects when Postgres server is not reachable", async () => { const postgresMonitor = new PostgresMonitorType(); const monitor = { databaseConnectionString: "http://localhost:15432", diff --git a/test/backend-test/monitors/test-rabbitmq.js b/test/backend-test/monitors/test-rabbitmq.js index 88a04afa1..63d358dfd 100644 --- a/test/backend-test/monitors/test-rabbitmq.js +++ b/test/backend-test/monitors/test-rabbitmq.js @@ -7,7 +7,7 @@ const { UP, PENDING } = require("../../../src/util"); describe("RabbitMQ Single Node", { skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), }, () => { - test("RabbitMQ is running", async () => { + test("check() sets status to UP when RabbitMQ server is reachable", async () => { // The default timeout of 30 seconds might not be enough for the container to start const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start(); const rabbitMQMonitor = new RabbitMqMonitorType(); @@ -33,7 +33,7 @@ describe("RabbitMQ Single Node", { } }); - test("RabbitMQ is not running", async () => { + test("check() rejects when RabbitMQ server is not reachable", async () => { const rabbitMQMonitor = new RabbitMqMonitorType(); const monitor = { rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]), diff --git a/test/backend-test/monitors/test-tcp.js b/test/backend-test/monitors/test-tcp.js index 71f0f2f87..76d093ff4 100644 --- a/test/backend-test/monitors/test-tcp.js +++ b/test/backend-test/monitors/test-tcp.js @@ -4,17 +4,7 @@ const { TCPMonitorType } = require("../../../server/monitor-types/tcp"); const { UP, PENDING } = require("../../../src/util"); const net = require("net"); -/** - * Test suite for TCP Monitor functionality - * This test suite checks the behavior of the TCPMonitorType class - * under different network connection scenarios. - */ describe("TCP Monitor", () => { - /** - * Creates a TCP server on a specified port - * @param {number} port - The port number to listen on - * @returns {Promise} A promise that resolves with the created server - */ async function createTCPServer(port) { return new Promise((resolve, reject) => { const server = net.createServer(); @@ -29,11 +19,7 @@ describe("TCP Monitor", () => { }); } - /** - * Test case to verify TCP monitor works when a server is running - * Checks that the monitor correctly identifies an active TCP server - */ - test("TCP server is running", async () => { + test("check() sets status to UP when TCP server is reachable", async () => { const port = 12345; const server = await createTCPServer(port); @@ -59,11 +45,7 @@ describe("TCP Monitor", () => { } }); - /** - * Test case to verify TCP monitor handles non-running servers - * Checks that the monitor correctly identifies an inactive TCP server - */ - test("TCP server is not running", async () => { + test("check() rejects with connection failed when TCP server is not running", async () => { const tcpMonitor = new TCPMonitorType(); const monitor = { @@ -83,11 +65,7 @@ describe("TCP Monitor", () => { ); }); - /** - * Test case to verify TCP monitor handles servers with expired or invalid TLS certificates - * Checks that the monitor correctly identifies TLS certificate issues - */ - test("TCP server with expired or invalid TLS certificate", async t => { + test("check() rejects when TLS certificate is expired or invalid", async () => { const tcpMonitor = new TCPMonitorType(); const monitor = { @@ -114,7 +92,7 @@ describe("TCP Monitor", () => { ); }); - test("TCP server with valid TLS certificate (SSL)", async t => { + test("check() sets status to UP when TLS certificate is valid (SSL)", async () => { const tcpMonitor = new TCPMonitorType(); const monitor = { @@ -137,7 +115,7 @@ describe("TCP Monitor", () => { assert.strictEqual(heartbeat.status, UP); }); - test("TCP server with valid TLS certificate (STARTTLS)", async t => { + test("check() sets status to UP when TLS certificate is valid (STARTTLS)", async () => { const tcpMonitor = new TCPMonitorType(); const monitor = { @@ -160,7 +138,7 @@ describe("TCP Monitor", () => { assert.strictEqual(heartbeat.status, UP); }); - test("TCP server with valid but name mismatching TLS certificate (STARTTLS)", async t => { + test("check() rejects when TLS certificate hostname does not match (STARTTLS)", async () => { const tcpMonitor = new TCPMonitorType(); const monitor = { @@ -185,7 +163,7 @@ describe("TCP Monitor", () => { regex ); }); - test("XMPP server with valid certificate (STARTTLS)", async t => { + test("check() sets status to UP for XMPP server with valid certificate (STARTTLS)", async () => { const tcpMonitor = new TCPMonitorType(); const monitor = { diff --git a/test/backend-test/monitors/test-websocket.js b/test/backend-test/monitors/test-websocket.js index 545cad466..d2923f1e7 100644 --- a/test/backend-test/monitors/test-websocket.js +++ b/test/backend-test/monitors/test-websocket.js @@ -22,9 +22,9 @@ function nonCompliantWS(port = 8080) { return new Promise((resolve) => srv.listen(port, () => resolve(srv))); } -describe("Websocket Test", { +describe("WebSocket Monitor", { }, () => { - test("Non WS Server", {}, async () => { + test("check() rejects with unexpected server response when connecting to non-WebSocket server", {}, async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -44,7 +44,7 @@ describe("Websocket Test", { ); }); - test("Secure WS", async () => { + test("check() sets status to UP when connecting to secure WebSocket server", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -68,7 +68,7 @@ describe("Websocket Test", { assert.deepStrictEqual(heartbeat, expected); }); - test("Insecure WS", async (t) => { + test("check() sets status to UP when connecting to insecure WebSocket server", async (t) => { t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); const wss = new WebSocketServer({ port: 8080 }); @@ -94,7 +94,7 @@ describe("Websocket Test", { assert.deepStrictEqual(heartbeat, expected); }); - test("Non compliant WS Server wrong status code", async () => { + test("check() rejects when status code does not match expected value", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -115,7 +115,7 @@ describe("Websocket Test", { ); }); - test("Secure WS Server no status code", async () => { + test("check() rejects when expected status code is empty", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -136,7 +136,7 @@ describe("Websocket Test", { ); }); - test("Non compliant WS server without IgnoreSecWebsocket", async (t) => { + test("check() rejects when Sec-WebSocket-Accept header is invalid", async (t) => { t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); const wss = await nonCompliantWS(); @@ -159,7 +159,7 @@ describe("Websocket Test", { ); }); - test("Non compliant WS server with IgnoreSecWebsocket", async (t) => { + test("check() sets status to UP when ignoring invalid Sec-WebSocket-Accept header", async (t) => { t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); const wss = await nonCompliantWS(); @@ -185,7 +185,7 @@ describe("Websocket Test", { assert.deepStrictEqual(heartbeat, expected); }); - test("Compliant WS server with IgnoreSecWebsocket", async () => { + test("check() sets status to UP for compliant WebSocket server when ignoring Sec-WebSocket-Accept", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -209,7 +209,7 @@ describe("Websocket Test", { assert.deepStrictEqual(heartbeat, expected); }); - test("Non WS server with IgnoreSecWebsocket", async () => { + test("check() rejects non-WebSocket server even when ignoring Sec-WebSocket-Accept", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -230,7 +230,7 @@ describe("Websocket Test", { ); }); - test("Secure WS no subprotocol support", async () => { + test("check() rejects when server does not support requested subprotocol", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -252,7 +252,7 @@ describe("Websocket Test", { ); }); - test("Multiple subprotocols invalid input", async () => { + test("check() rejects when multiple subprotocols contain invalid characters", async () => { const websocketMonitor = new WebSocketMonitorType(); const monitor = { @@ -274,7 +274,7 @@ describe("Websocket Test", { ); }); - test("Insecure WS subprotocol multiple spaces", async (t) => { + test("check() sets status to UP when subprotocol with multiple spaces is accepted", async (t) => { t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); const wss = new WebSocketServer({ port: 8080, @@ -305,7 +305,7 @@ describe("Websocket Test", { assert.deepStrictEqual(heartbeat, expected); }); - test("Insecure WS supports one subprotocol", async (t) => { + test("check() sets status to UP when server supports requested subprotocol", async (t) => { t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); const wss = new WebSocketServer({ port: 8080, diff --git a/test/backend-test/test-migration.js b/test/backend-test/test-migration.js index 87e5ff9df..19b00dad7 100644 --- a/test/backend-test/test-migration.js +++ b/test/backend-test/test-migration.js @@ -3,7 +3,7 @@ const fs = require("fs"); const path = require("path"); const { GenericContainer, Wait } = require("testcontainers"); -describe("Database Migration - Optimize Important Indexes", () => { +describe("Database Migration", () => { test("SQLite: All migrations run successfully", async () => { const testDbPath = path.join(__dirname, "../../data/test-migration.db"); const testDbDir = path.dirname(testDbPath); From 31285004baf97b9b027df14e282a9c1a184191d3 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 17:06:05 +0100 Subject: [PATCH 20/35] move the maintenance to the utils test --- test/backend-test/test-maintenance.js | 63 --------------------------- test/backend-test/test-util.js | 20 +++++++++ 2 files changed, 20 insertions(+), 63 deletions(-) delete mode 100644 test/backend-test/test-maintenance.js diff --git a/test/backend-test/test-maintenance.js b/test/backend-test/test-maintenance.js deleted file mode 100644 index 27f3ff6b9..000000000 --- a/test/backend-test/test-maintenance.js +++ /dev/null @@ -1,63 +0,0 @@ -const { describe, test } = require("node:test"); -const assert = require("node:assert"); -const dayjs = require("dayjs"); -const { SQL_DATETIME_FORMAT } = require("../../src/util"); - -dayjs.extend(require("dayjs/plugin/utc")); -dayjs.extend(require("dayjs/plugin/customParseFormat")); - -/** - * Tests for maintenance date formatting to ensure compatibility with MariaDB/MySQL. - * Issue: MariaDB rejects ISO format dates like '2025-12-19T01:04:02.129Z' - * Fix: Use SQL_DATETIME_FORMAT ('YYYY-MM-DD HH:mm:ss') instead of toISOString() - */ -describe("Maintenance Date Format - MariaDB Compatibility", () => { - test("SQL_DATETIME_FORMAT constant should match MariaDB format", () => { - assert.strictEqual(SQL_DATETIME_FORMAT, "YYYY-MM-DD HH:mm:ss"); - }); - - test("format() produces SQL-compatible datetime string", () => { - const current = dayjs.utc("2025-12-19T01:04:02.129Z"); - const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); - - assert.strictEqual(sqlFormat, "2025-12-19 01:04:02"); - }); - - test("SQL format does not contain ISO markers (T, Z)", () => { - const current = dayjs.utc("2025-12-19T01:04:02.129Z"); - const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); - - assert.strictEqual(sqlFormat.includes("T"), false, "SQL format should not contain 'T'"); - assert.strictEqual(sqlFormat.includes("Z"), false, "SQL format should not contain 'Z'"); - }); - - test("SQL format matches YYYY-MM-DD HH:mm:ss pattern", () => { - const current = dayjs.utc("2025-12-19T01:04:02.129Z"); - const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); - const sqlDateTimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; - - assert.strictEqual(sqlDateTimeRegex.test(sqlFormat), true); - }); - - test("parsing SQL datetime back to dayjs preserves timestamp", () => { - const originalDate = dayjs.utc("2025-12-19T01:04:02.000Z"); - const sqlFormat = originalDate.utc().format(SQL_DATETIME_FORMAT); - const parsedDate = dayjs.utc(sqlFormat, SQL_DATETIME_FORMAT); - - assert.strictEqual(parsedDate.unix(), originalDate.unix()); - }); - - test("formats midnight timestamp correctly", () => { - const midnight = dayjs.utc("2025-01-01T00:00:00.000Z"); - const sqlFormat = midnight.utc().format(SQL_DATETIME_FORMAT); - - assert.strictEqual(sqlFormat, "2025-01-01 00:00:00"); - }); - - test("formats end of day timestamp correctly", () => { - const endOfDay = dayjs.utc("2025-12-31T23:59:59.999Z"); - const sqlFormat = endOfDay.utc().format(SQL_DATETIME_FORMAT); - - assert.strictEqual(sqlFormat, "2025-12-31 23:59:59"); - }); -}); diff --git a/test/backend-test/test-util.js b/test/backend-test/test-util.js index 63660d4fb..a034df11c 100644 --- a/test/backend-test/test-util.js +++ b/test/backend-test/test-util.js @@ -1,7 +1,12 @@ const { describe, test } = require("node:test"); const assert = require("node:assert"); +const dayjs = require("dayjs"); const { getDaysRemaining, getDaysBetween } = require("../../server/util-server"); +const { SQL_DATETIME_FORMAT } = require("../../src/util"); + +dayjs.extend(require("dayjs/plugin/utc")); +dayjs.extend(require("dayjs/plugin/customParseFormat")); describe("Server Utilities", () => { test("getDaysBetween() calculates days between dates within same month", () => { @@ -23,4 +28,19 @@ describe("Server Utilities", () => { const days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7)); assert.strictEqual(days, -3); }); + + test("SQL_DATETIME_FORMAT constant matches MariaDB/MySQL format", () => { + assert.strictEqual(SQL_DATETIME_FORMAT, "YYYY-MM-DD HH:mm:ss"); + }); + + test("SQL_DATETIME_FORMAT produces valid SQL datetime string", () => { + const current = dayjs.utc("2025-12-19T01:04:02.129Z"); + const sqlFormat = current.utc().format(SQL_DATETIME_FORMAT); + + assert.strictEqual(sqlFormat, "2025-12-19 01:04:02"); + + // Verify it can be parsed back + const parsedDate = dayjs.utc(sqlFormat, SQL_DATETIME_FORMAT); + assert.strictEqual(parsedDate.unix(), current.unix()); + }); }); From 5f0b1f5e4d4ac59b5f10618beecce5edf896a01b Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 17:06:29 +0100 Subject: [PATCH 21/35] improve naming for migration tests --- test/backend-test/test-migration.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/backend-test/test-migration.js b/test/backend-test/test-migration.js index 19b00dad7..0ed494fae 100644 --- a/test/backend-test/test-migration.js +++ b/test/backend-test/test-migration.js @@ -4,7 +4,7 @@ const path = require("path"); const { GenericContainer, Wait } = require("testcontainers"); describe("Database Migration", () => { - test("SQLite: All migrations run successfully", async () => { + test("SQLite migrations run successfully from fresh database", async () => { const testDbPath = path.join(__dirname, "../../data/test-migration.db"); const testDbDir = path.dirname(testDbPath); @@ -57,7 +57,7 @@ describe("Database Migration", () => { }); test( - "MariaDB: All migrations run successfully", + "MariaDB migrations run successfully from fresh database", { skip: !!process.env.CI && From cf1e17f5875fb819490a9a18b0a3381bd174aa79 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Thu, 1 Jan 2026 17:08:36 +0100 Subject: [PATCH 22/35] fix lint --- test/backend-test/monitors/test-tcp.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/backend-test/monitors/test-tcp.js b/test/backend-test/monitors/test-tcp.js index 76d093ff4..4626fe34f 100644 --- a/test/backend-test/monitors/test-tcp.js +++ b/test/backend-test/monitors/test-tcp.js @@ -5,6 +5,11 @@ const { UP, PENDING } = require("../../../src/util"); const net = require("net"); describe("TCP Monitor", () => { + /** + * Creates a TCP server on a specified port + * @param {number} port - The port number to listen on + * @returns {Promise} A promise that resolves with the created server + */ async function createTCPServer(port) { return new Promise((resolve, reject) => { const server = net.createServer(); From 44cc5e08a77c1e02510417d093fa6ffcb549ef97 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:37:58 +0100 Subject: [PATCH 23/35] pin gha and add dependabot to update them --- .github/dependabot.yml | 20 +++++++++++++++++ .github/workflows/auto-test.yml | 24 ++++++++++----------- .github/workflows/close-incorrect-issue.yml | 4 ++-- .github/workflows/codeql-analysis.yml | 8 +++---- .github/workflows/conflict_labeler.yml | 2 +- .github/workflows/prevent-file-change.yml | 2 +- .github/workflows/stale-bot.yml | 4 ++-- .github/workflows/validate.yml | 8 +++---- 8 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..73702c5fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# Dependabot configuration for Uptime Kuma +# See: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + # Group all GitHub Actions updates into a single PR + groups: + github-actions: + patterns: + - "*" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore" + include: "scope" \ No newline at end of file diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index f0dfdfa55..cf3393006 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -31,17 +31,17 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache/Restore node_modules - uses: actions/cache@v4 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 id: node-modules-cache with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node }} - run: npm install @@ -65,17 +65,17 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache/Restore node_modules - uses: actions/cache@v4 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 id: node-modules-cache with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node }} - run: npm install --production @@ -85,17 +85,17 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache/Restore node_modules - uses: actions/cache@v4 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 id: node-modules-cache with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} - name: Use Node.js 20 - uses: actions/setup-node@v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 20 - run: npm install @@ -108,17 +108,17 @@ jobs: PLAYWRIGHT_VERSION: ~1.39.0 steps: - run: git config --global core.autocrlf false # Mainly for Windows - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache/Restore node_modules - uses: actions/cache@v4 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 id: node-modules-cache with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 22 - run: npm install diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml index f618cd7c2..66266dd0a 100644 --- a/.github/workflows/close-incorrect-issue.yml +++ b/.github/workflows/close-incorrect-issue.yml @@ -14,10 +14,10 @@ jobs: node-version: [20] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: ${{ matrix.node-version }} - run: npm ci diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0e3b72c4b..a26befa0e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,18 +26,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml index fdcc9c551..a2a4d9a42 100644 --- a/.github/workflows/conflict_labeler.yml +++ b/.github/workflows/conflict_labeler.yml @@ -19,7 +19,7 @@ jobs: pull-requests: write steps: - name: Apply label - uses: eps1lon/actions-label-merge-conflict@v3 + uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 with: dirtyLabel: 'needs:resolve-merge-conflict' repoToken: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/prevent-file-change.yml b/.github/workflows/prevent-file-change.yml index 0af3a6cbf..21e6cc16b 100644 --- a/.github/workflows/prevent-file-change.yml +++ b/.github/workflows/prevent-file-change.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prevent file change - uses: xalvarez/prevent-file-change-action@v1 + uses: xalvarez/prevent-file-change-action@8ba6c9f0f3c6c73caea35ae4b13988047f9cd104 # v3.0.0 with: githubToken: ${{ secrets.GITHUB_TOKEN }} # Regex, /src/lang/*.json is not allowed to be changed, except for /src/lang/en.json diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 60eca6403..50c63f314 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -9,7 +9,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: stale-issue-message: |- We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity. @@ -21,7 +21,7 @@ jobs: exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request' exempt-issue-assignees: 'louislam' operations-per-run: 200 - - uses: actions/stale@v9 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: stale-issue-message: |- This issue was marked as `cannot-reproduce` by a maintainer. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 4dff3689d..1fe5b3606 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,11 +17,11 @@ jobs: json-yaml-validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: json-yaml-validate id: json-yaml-validate - uses: GrantBirki/json-yaml-validate@v2.4.0 + uses: GrantBirki/json-yaml-validate@9bbaa8474e3af4e91f25eda8ac194fdc30564d96 # v4.0.0 with: comment: "true" # enable comment mode exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions @@ -30,9 +30,9 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Use Node.js 20 - uses: actions/setup-node@v6 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 20 From 373ebe2df7c83c8d1aa2ba89ca4dbcadeac38e57 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:41:05 +0100 Subject: [PATCH 24/35] Update prevent-file-change.yml --- .github/workflows/prevent-file-change.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prevent-file-change.yml b/.github/workflows/prevent-file-change.yml index 21e6cc16b..fcb47a659 100644 --- a/.github/workflows/prevent-file-change.yml +++ b/.github/workflows/prevent-file-change.yml @@ -1,11 +1,14 @@ name: prevent-file-change on: - pull_request: + pull_request_target: +permissions: {} jobs: check-file-changes: runs-on: ubuntu-latest + permissions: + pull-requests: read steps: - name: Prevent file change uses: xalvarez/prevent-file-change-action@8ba6c9f0f3c6c73caea35ae4b13988047f9cd104 # v3.0.0 From 3846333ffbb98f51f7c4dd29d991b4cccac2cd56 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:48:31 +0100 Subject: [PATCH 25/35] Update .github/dependabot.yml --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 73702c5fc..70efa7782 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,4 +17,4 @@ updates: open-pull-requests-limit: 5 commit-message: prefix: "chore" - include: "scope" \ No newline at end of file + include: "scope" From ba26a319425a176ab6ef0cf250dd8daa489b0893 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:49:26 +0100 Subject: [PATCH 26/35] add a cooldown --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 70efa7782..90eb809ab 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,3 +18,5 @@ updates: commit-message: prefix: "chore" include: "scope" + cooldown: + default-days: 7 From 77f52e4d47492a23b25c95a6b969b502db549e43 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:51:43 +0100 Subject: [PATCH 27/35] don't persist credentials in CI --- .github/workflows/auto-test.yml | 4 ++++ .github/workflows/close-incorrect-issue.yml | 1 + .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/validate.yml | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index cf3393006..48496acf8 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -32,6 +32,7 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: Cache/Restore node_modules uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 @@ -66,6 +67,7 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: Cache/Restore node_modules uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 @@ -86,6 +88,7 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: Cache/Restore node_modules uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 @@ -109,6 +112,7 @@ jobs: steps: - run: git config --global core.autocrlf false # Mainly for Windows - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: Cache/Restore node_modules uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml index 66266dd0a..8c50345cc 100644 --- a/.github/workflows/close-incorrect-issue.yml +++ b/.github/workflows/close-incorrect-issue.yml @@ -15,6 +15,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a26befa0e..aa0b3c860 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,8 +25,8 @@ jobs: language: [ 'go', 'javascript-typescript' ] steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1fe5b3606..e1d760b52 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -18,6 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: json-yaml-validate id: json-yaml-validate @@ -31,6 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: { persist-credentials: false } - name: Use Node.js 20 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: From 1a5b9894961adb8f133e3b5761ba593663912e54 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:55:14 +0100 Subject: [PATCH 28/35] tighten permissions for stalebot --- .github/workflows/stale-bot.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 50c63f314..8cb8dd55d 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -4,10 +4,14 @@ on: schedule: - cron: '0 */6 * * *' #Run every 6 hours +permissions: {} jobs: stale: runs-on: ubuntu-latest + permissions: + actions: write + issues: write steps: - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: @@ -18,7 +22,7 @@ jobs: days-before-close: 7 days-before-pr-stale: -1 days-before-pr-close: -1 - exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request' + exempt-issue-labels: 'News,discussion,bug,doc,feature-request' exempt-issue-assignees: 'louislam' operations-per-run: 200 - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 From c37d019f436289c93a3482f8ec03efc5ea6cd6b6 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:58:07 +0100 Subject: [PATCH 29/35] add more permission blocks --- .github/workflows/auto-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index 48496acf8..0a154cc74 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -84,6 +84,8 @@ jobs: check-linters: runs-on: ubuntu-latest + permissions: + contents: read steps: - run: git config --global core.autocrlf false # Mainly for Windows @@ -107,6 +109,8 @@ jobs: e2e-test: needs: [ check-linters ] runs-on: ARM64 + permissions: + contents: read env: PLAYWRIGHT_VERSION: ~1.39.0 steps: From 7141d16a3331b825b4be3639a66360cb543aab70 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 02:59:20 +0100 Subject: [PATCH 30/35] add more permissions --- .github/workflows/auto-test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index 0a154cc74..c88a5d695 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -12,12 +12,15 @@ on: branches: [ master, 1.23.X ] paths-ignore: - '*.md' +permissions: {} jobs: auto-test: needs: [ e2e-test ] runs-on: ${{ matrix.os }} timeout-minutes: 15 + permissions: + contents: read strategy: matrix: @@ -57,6 +60,8 @@ jobs: needs: [ e2e-test ] runs-on: ${{ matrix.os }} timeout-minutes: 15 + permissions: + contents: read if: ${{ github.repository == 'louislam/uptime-kuma' }} strategy: matrix: From a603b8e7d3317f89df7c78ed108540f20ffd82e9 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 03:01:35 +0100 Subject: [PATCH 31/35] more permissions --- .github/workflows/validate.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e1d760b52..3da0f9060 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,14 +8,14 @@ on: - master - 1.23.X workflow_dispatch: - -permissions: - contents: read - pull-requests: write # enable write permissions for pull request comments +permissions: {} jobs: json-yaml-validate: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # enable write permissions for pull request comments steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: { persist-credentials: false } @@ -30,6 +30,8 @@ jobs: # General validations validate: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: { persist-credentials: false } From a5a20ac075aa12280d5db937dca9e36f9ea5b03a Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 03:07:34 +0100 Subject: [PATCH 32/35] fix a few more CI issues --- .github/workflows/close-incorrect-issue.yml | 8 +++++++- .github/workflows/conflict_labeler.yml | 7 ++++++- .github/workflows/prevent-file-change.yml | 7 ++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml index 8c50345cc..aa7113ed7 100644 --- a/.github/workflows/close-incorrect-issue.yml +++ b/.github/workflows/close-incorrect-issue.yml @@ -3,10 +3,13 @@ name: Close Incorrect Issue on: issues: types: [opened] +permissions: {} jobs: close-incorrect-issue: runs-on: ${{ matrix.os }} + permissions: + issues: write strategy: matrix: @@ -22,4 +25,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm ci - - run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} ${{ github.event.issue.user.login }} + - name: Close incorrect issue + run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} "$ISSUE_USER_LOGIN" + env: + ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }} diff --git a/.github/workflows/conflict_labeler.yml b/.github/workflows/conflict_labeler.yml index a2a4d9a42..65634d11e 100644 --- a/.github/workflows/conflict_labeler.yml +++ b/.github/workflows/conflict_labeler.yml @@ -1,6 +1,11 @@ name: Merge Conflict Labeler -on: +# pull_request_target is safe here because: +# 1. Only uses a pinned trusted action (by SHA) +# 2. Has minimal permissions (contents: read, pull-requests: write) +# 3. Doesn't checkout or execute any untrusted code from PRs +# 4. Only adds/removes labels based on merge conflict status +on: # zizmor: ignore[dangerous-triggers] push: branches: - master diff --git a/.github/workflows/prevent-file-change.yml b/.github/workflows/prevent-file-change.yml index fcb47a659..95f099759 100644 --- a/.github/workflows/prevent-file-change.yml +++ b/.github/workflows/prevent-file-change.yml @@ -1,6 +1,11 @@ name: prevent-file-change -on: +# pull_request_target is safe here because: +# 1. Only uses a pinned trusted action (by SHA) +# 2. Has minimal permissions (pull-requests: read) +# 3. Doesn't checkout or execute any untrusted code from PRs +# 4. Only validates that language files (except en.json) aren't modified +on: # zizmor: ignore[dangerous-triggers] pull_request_target: permissions: {} From 289fee40bbac6fb70adc5ed50a89def4a6e4ae21 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 03:29:23 +0100 Subject: [PATCH 33/35] Apply suggestions from code review --- .github/workflows/prevent-file-change.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/prevent-file-change.yml b/.github/workflows/prevent-file-change.yml index 95f099759..3c48dec1b 100644 --- a/.github/workflows/prevent-file-change.yml +++ b/.github/workflows/prevent-file-change.yml @@ -1,12 +1,7 @@ name: prevent-file-change -# pull_request_target is safe here because: -# 1. Only uses a pinned trusted action (by SHA) -# 2. Has minimal permissions (pull-requests: read) -# 3. Doesn't checkout or execute any untrusted code from PRs -# 4. Only validates that language files (except en.json) aren't modified -on: # zizmor: ignore[dangerous-triggers] - pull_request_target: +on: + pull_request: permissions: {} jobs: @@ -16,7 +11,7 @@ jobs: pull-requests: read steps: - name: Prevent file change - uses: xalvarez/prevent-file-change-action@8ba6c9f0f3c6c73caea35ae4b13988047f9cd104 # v3.0.0 + uses: xalvarez/prevent-file-change-action@004d9f17c2e4a7afa037cda5f38dc55a5e9c9c06 # v1.9.1 with: githubToken: ${{ secrets.GITHUB_TOKEN }} # Regex, /src/lang/*.json is not allowed to be changed, except for /src/lang/en.json From 1d0415f66497128a95e81efdf17ee053c6375f7e Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 05:48:27 +0100 Subject: [PATCH 34/35] fix(ci): update OS versions in auto-test workflow to use github hosted instead of selfhosted runners (#6567) --- .github/workflows/auto-test.yml | 44 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/.github/workflows/auto-test.yml b/.github/workflows/auto-test.yml index c88a5d695..e86d7dd00 100644 --- a/.github/workflows/auto-test.yml +++ b/.github/workflows/auto-test.yml @@ -16,7 +16,6 @@ permissions: {} jobs: auto-test: - needs: [ e2e-test ] runs-on: ${{ matrix.os }} timeout-minutes: 15 permissions: @@ -24,7 +23,7 @@ jobs: strategy: matrix: - os: [macos-latest, ubuntu-22.04, windows-latest, ARM64] + os: [macos-latest, ubuntu-22.04, windows-latest, ubuntu-22.04-arm] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ node: [ 20, 24 ] # Also test non-LTS, but only on Ubuntu. @@ -49,6 +48,11 @@ jobs: with: node-version: ${{ matrix.node }} - run: npm install + + - name: Rebuild native modules for ARM64 + if: matrix.os == 'ubuntu-22.04-arm' + run: npm rebuild @louislam/sqlite3 + - run: npm run build - run: npm run test-backend env: @@ -57,35 +61,33 @@ jobs: # As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works armv7-simple-test: - needs: [ e2e-test ] - runs-on: ${{ matrix.os }} - timeout-minutes: 15 + runs-on: ubuntu-latest permissions: contents: read - if: ${{ github.repository == 'louislam/uptime-kuma' }} strategy: matrix: - os: [ ARMv7 ] node: [ 20, 22 ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - run: git config --global core.autocrlf false # Mainly for Windows - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: { persist-credentials: false } - - name: Cache/Restore node_modules - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 - id: node-modules-cache + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 with: - path: node_modules - key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + platforms: linux/arm/v7 - - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 - with: - node-version: ${{ matrix.node }} - - run: npm install --production + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + + - name: Test on ARMv7 using Docker with QEMU + run: | + docker run --rm --platform linux/arm/v7 \ + -v $PWD:/workspace \ + -w /workspace \ + arm32v7/node:${{ matrix.node }}-slim \ + bash -c "npm install --production" check-linters: runs-on: ubuntu-latest @@ -112,8 +114,7 @@ jobs: - run: npm run lint:prod e2e-test: - needs: [ check-linters ] - runs-on: ARM64 + runs-on: ubuntu-22.04-arm permissions: contents: read env: @@ -135,6 +136,9 @@ jobs: with: node-version: 22 - run: npm install + + - name: Rebuild native modules for ARM64 + run: npm rebuild @louislam/sqlite3 - name: Install Playwright ${{ env.PLAYWRIGHT_VERSION }} run: npx playwright@${{ env.PLAYWRIGHT_VERSION }} install From 6a95a882139926a2b625e713669536e92fe53b31 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Fri, 2 Jan 2026 06:40:27 +0100 Subject: [PATCH 35/35] Update server/model/status_page.js --- server/model/status_page.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/model/status_page.js b/server/model/status_page.js index 9842f0019..7087e4e09 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -135,11 +135,6 @@ class StatusPage extends BeanModel { let ogType = $(""); head.append(ogType); - if (statusPage.icon) { - let ogImage = $("").attr("content", statusPage.icon); - head.append(ogImage); - } - // Preload data // Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186 const escapedJSONObject = jsesc(await StatusPage.getStatusPageData(statusPage), {