Merge branch 'master' into pin-gha

This commit is contained in:
Frank Elsinga 2026-01-02 02:45:05 +01:00 committed by GitHub
commit 4e2b63a6c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 725 additions and 689 deletions

View File

@ -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); return getRdapDomainExpiryDate(this.domain);
} }

View File

@ -1223,5 +1223,6 @@
"labelDomainNameExpiryNotification": "Domain Name Expiry Notification", "labelDomainNameExpiryNotification": "Domain Name Expiry Notification",
"domainExpiryDescription": "Trigger notification when domain names expires in:", "domainExpiryDescription": "Trigger notification when domain names expires in:",
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.", "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.",
"imageResetConfirmation": "Image reset to default"
} }

View File

@ -138,6 +138,9 @@
<h1 class="mb-4 title-flex"> <h1 class="mb-4 title-flex">
<!-- Logo --> <!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod"> <span class="logo-wrapper" @click="showImageCropUploadMethod">
<button v-if="editMode" type="button" class="p-0 bg-transparent border-0 small-reset-btn reset-top-left" @click.stop="resetToDefaultImage">
<font-awesome-icon icon="times" class="text-danger" />
</button>
<img :src="logoURL" alt class="logo me-2" :class="logoClass" /> <img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span> </span>
@ -962,6 +965,20 @@ 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("imageResetConfirmation"));
},
/** /**
* Create an incident for this status page * Create an incident for this status page
* @returns {void} * @returns {void}
@ -1181,6 +1198,58 @@ footer {
cursor: pointer; cursor: pointer;
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); 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 { .logo {

View File

@ -4,14 +4,26 @@ Documentation: https://nodejs.org/api/test.html
Create a test file in this directory with the name `*.js`. 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 ## Template
```js ```js
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
test("Test name", async (t) => { describe("Feature Name", () => {
assert.strictEqual(1, 1); test("function() returns expected value when condition is met", () => {
assert.strictEqual(1, 1);
});
}); });
``` ```

View File

@ -1,46 +1,48 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js"); const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js");
const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js"); const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js");
test("Test evaluateExpression", async (t) => { describe("Expression Evaluator", () => {
const expr = new ConditionExpression("record", "contains", "mx1.example.com"); test("evaluateExpression() returns true when condition matches and false otherwise", () => {
assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" })); const expr = new ConditionExpression("record", "contains", "mx1.example.com");
assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.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) => { test("evaluateExpressionGroup() with AND logic requires all conditions to be true", () => {
const group = new ConditionExpressionGroup([ const group = new ConditionExpressionGroup([
new ConditionExpression("record", "contains", "mx1."), new ConditionExpression("record", "contains", "mx1."),
new ConditionExpression("record", "contains", "example.com", LOGICAL.AND), new ConditionExpression("record", "contains", "example.com", LOGICAL.AND),
]); ]);
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" })); assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
}); });
test("Test evaluateExpressionGroup with logical OR", async (t) => { test("evaluateExpressionGroup() with OR logic requires at least one condition to be true", () => {
const group = new ConditionExpressionGroup([ 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([
new ConditionExpression("record", "contains", "example.com"), new ConditionExpression("record", "contains", "example.com"),
new ConditionExpression("record", "contains", "example.org", LOGICAL.OR), new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
]), ]);
]); assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." })); assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" })); assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
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" })); test("evaluateExpressionGroup() evaluates nested groups correctly", () => {
assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" })); 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" }));
});
}); });

View File

@ -1,108 +1,110 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); 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"); 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) => { describe("Expression Operators", () => {
const op = operatorMap.get(OP_STR_EQUALS); test("StringEqualsOperator returns true for identical strings and false otherwise", () => {
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com")); const op = operatorMap.get(OP_STR_EQUALS);
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com"));
assert.strictEqual(false, op.test("1", 1)); // strict equality 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) => { test("StringNotEqualsOperator returns true for different strings and false for identical strings", () => {
const op = operatorMap.get(OP_STR_NOT_EQUALS); const op = operatorMap.get(OP_STR_NOT_EQUALS);
assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org")); assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org"));
assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com")); assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com"));
assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality) assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality)
}); });
test("Test ContainsOperator with scalar", async (t) => { test("ContainsOperator returns true when scalar contains substring", () => {
const op = operatorMap.get(OP_CONTAINS); const op = operatorMap.get(OP_CONTAINS);
assert.strictEqual(true, op.test("mx1.example.org", "example.org")); assert.strictEqual(true, op.test("mx1.example.org", "example.org"));
assert.strictEqual(false, op.test("mx1.example.org", "example.com")); assert.strictEqual(false, op.test("mx1.example.org", "example.com"));
}); });
test("Test ContainsOperator with array", async (t) => { test("ContainsOperator returns true when array contains element", () => {
const op = operatorMap.get(OP_CONTAINS); const op = operatorMap.get(OP_CONTAINS);
assert.strictEqual(true, op.test([ "example.org" ], "example.org")); assert.strictEqual(true, op.test([ "example.org" ], "example.org"));
assert.strictEqual(false, op.test([ "example.org" ], "example.com")); assert.strictEqual(false, op.test([ "example.org" ], "example.com"));
}); });
test("Test NotContainsOperator with scalar", async (t) => { test("NotContainsOperator returns true when scalar does not contain substring", () => {
const op = operatorMap.get(OP_NOT_CONTAINS); const op = operatorMap.get(OP_NOT_CONTAINS);
assert.strictEqual(true, op.test("example.org", ".com")); assert.strictEqual(true, op.test("example.org", ".com"));
assert.strictEqual(false, op.test("example.org", ".org")); assert.strictEqual(false, op.test("example.org", ".org"));
}); });
test("Test NotContainsOperator with array", async (t) => { test("NotContainsOperator returns true when array does not contain element", () => {
const op = operatorMap.get(OP_NOT_CONTAINS); const op = operatorMap.get(OP_NOT_CONTAINS);
assert.strictEqual(true, op.test([ "example.org" ], "example.com")); assert.strictEqual(true, op.test([ "example.org" ], "example.com"));
assert.strictEqual(false, op.test([ "example.org" ], "example.org")); assert.strictEqual(false, op.test([ "example.org" ], "example.org"));
}); });
test("Test StartsWithOperator", async (t) => { test("StartsWithOperator returns true when string starts with prefix", () => {
const op = operatorMap.get(OP_STARTS_WITH); const op = operatorMap.get(OP_STARTS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "mx1")); assert.strictEqual(true, op.test("mx1.example.com", "mx1"));
assert.strictEqual(false, op.test("mx1.example.com", "mx2")); assert.strictEqual(false, op.test("mx1.example.com", "mx2"));
}); });
test("Test NotStartsWithOperator", async (t) => { test("NotStartsWithOperator returns true when string does not start with prefix", () => {
const op = operatorMap.get(OP_NOT_STARTS_WITH); const op = operatorMap.get(OP_NOT_STARTS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "mx2")); assert.strictEqual(true, op.test("mx1.example.com", "mx2"));
assert.strictEqual(false, op.test("mx1.example.com", "mx1")); assert.strictEqual(false, op.test("mx1.example.com", "mx1"));
}); });
test("Test EndsWithOperator", async (t) => { test("EndsWithOperator returns true when string ends with suffix", () => {
const op = operatorMap.get(OP_ENDS_WITH); const op = operatorMap.get(OP_ENDS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "example.com")); assert.strictEqual(true, op.test("mx1.example.com", "example.com"));
assert.strictEqual(false, op.test("mx1.example.com", "example.net")); assert.strictEqual(false, op.test("mx1.example.com", "example.net"));
}); });
test("Test NotEndsWithOperator", async (t) => { test("NotEndsWithOperator returns true when string does not end with suffix", () => {
const op = operatorMap.get(OP_NOT_ENDS_WITH); const op = operatorMap.get(OP_NOT_ENDS_WITH);
assert.strictEqual(true, op.test("mx1.example.com", "example.net")); assert.strictEqual(true, op.test("mx1.example.com", "example.net"));
assert.strictEqual(false, op.test("mx1.example.com", "example.com")); assert.strictEqual(false, op.test("mx1.example.com", "example.com"));
}); });
test("Test NumberEqualsOperator", async (t) => { test("NumberEqualsOperator returns true for equal numbers with type coercion", () => {
const op = operatorMap.get(OP_NUM_EQUALS); const op = operatorMap.get(OP_NUM_EQUALS);
assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 1));
assert.strictEqual(true, op.test(1, "1")); assert.strictEqual(true, op.test(1, "1"));
assert.strictEqual(false, op.test(1, "2")); assert.strictEqual(false, op.test(1, "2"));
}); });
test("Test NumberNotEqualsOperator", async (t) => { test("NumberNotEqualsOperator returns true for different numbers", () => {
const op = operatorMap.get(OP_NUM_NOT_EQUALS); const op = operatorMap.get(OP_NUM_NOT_EQUALS);
assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(true, op.test(1, "2"));
assert.strictEqual(false, op.test(1, "1")); assert.strictEqual(false, op.test(1, "1"));
}); });
test("Test LessThanOperator", async (t) => { test("LessThanOperator returns true when first number is less than second", () => {
const op = operatorMap.get(OP_LT); const op = operatorMap.get(OP_LT);
assert.strictEqual(true, op.test(1, 2)); assert.strictEqual(true, op.test(1, 2));
assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(true, op.test(1, "2"));
assert.strictEqual(false, op.test(1, 1)); assert.strictEqual(false, op.test(1, 1));
}); });
test("Test GreaterThanOperator", async (t) => { test("GreaterThanOperator returns true when first number is greater than second", () => {
const op = operatorMap.get(OP_GT); const op = operatorMap.get(OP_GT);
assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, 1));
assert.strictEqual(true, op.test(2, "1")); assert.strictEqual(true, op.test(2, "1"));
assert.strictEqual(false, op.test(1, 1)); assert.strictEqual(false, op.test(1, 1));
}); });
test("Test LessThanOrEqualToOperator", async (t) => { test("LessThanOrEqualToOperator returns true when first number is less than or equal to second", () => {
const op = operatorMap.get(OP_LTE); const op = operatorMap.get(OP_LTE);
assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 1));
assert.strictEqual(true, op.test(1, 2)); assert.strictEqual(true, op.test(1, 2));
assert.strictEqual(true, op.test(1, "2")); assert.strictEqual(true, op.test(1, "2"));
assert.strictEqual(false, op.test(1, 0)); assert.strictEqual(false, op.test(1, 0));
}); });
test("Test GreaterThanOrEqualToOperator", async (t) => { test("GreaterThanOrEqualToOperator returns true when first number is greater than or equal to second", () => {
const op = operatorMap.get(OP_GTE); const op = operatorMap.get(OP_GTE);
assert.strictEqual(true, op.test(1, 1)); assert.strictEqual(true, op.test(1, 1));
assert.strictEqual(true, op.test(2, 1)); assert.strictEqual(true, op.test(2, 1));
assert.strictEqual(true, op.test(2, "2")); assert.strictEqual(true, op.test(2, "2"));
assert.strictEqual(false, op.test(2, 3)); assert.strictEqual(false, op.test(2, 3));
});
}); });

View File

@ -2,8 +2,8 @@ const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const grpc = require("@grpc/grpc-js"); const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader"); const protoLoader = require("@grpc/proto-loader");
const { GrpcKeywordMonitorType } = require("../../server/monitor-types/grpc"); const { GrpcKeywordMonitorType } = require("../../../server/monitor-types/grpc");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const os = require("os"); const os = require("os");
@ -82,7 +82,7 @@ async function createTestGrpcServer(port, methodHandlers) {
describe("GrpcKeywordMonitorType", { describe("GrpcKeywordMonitorType", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), 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 port = 50051;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { 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 port = 50052;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { 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 port = 50053;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { 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 port = 50054;
const server = await createTestGrpcServer(port, { const server = await createTestGrpcServer(port, {
Echo: (call, callback) => { 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 grpcMonitor = new GrpcKeywordMonitorType();
const monitor = { const monitor = {
grpcUrl: "localhost:50099", 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 port = 50055;
const longMessage = "A".repeat(100) + " with SUCCESS keyword"; const longMessage = "A".repeat(100) + " with SUCCESS keyword";

View File

@ -2,8 +2,8 @@ const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq"); const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt"); const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt"); const { MqttMonitorType } = require("../../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
/** /**
* Runs an MQTT test with the * Runs an MQTT test with the
@ -58,91 +58,91 @@ describe("MqttMonitorType", {
concurrency: 4, concurrency: 4,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64") 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 <-"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); 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"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/b/c", "a/b/c");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); 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/'/$/./*/%"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/'/$/./*/%", "a/'/$/./*/%");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/'/$/./*/%; Message: -> KEYWORD <-"); 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"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/#", "a/b/c");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); 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"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c", "a/b/c");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c; Message: -> KEYWORD <-"); 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"); const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-", "a/+/c/#", "a/b/c/d/e");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: a/b/c/d/e; Message: -> KEYWORD <-"); 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( await assert.rejects(
testMqtt("keyword will not be checked anyway", null, "message", "x/y/z", "a/b/c"), testMqtt("keyword will not be checked anyway", null, "message", "x/y/z", "a/b/c"),
new Error("Timeout, Message not received"), 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( await assert.rejects(
testMqtt("", null, "# should be last character", "#/c", "a/b/c"), testMqtt("", null, "# should be last character", "#/c", "a/b/c"),
new Error("Timeout, Message not received"), 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( await assert.rejects(
testMqtt("", null, "message", "x/+/z", "a/b/c"), testMqtt("", null, "message", "x/+/z", "a/b/c"),
new Error("Timeout, Message not received"), 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 <-"); const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-"); 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( await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"), testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> 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( await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"), testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> 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" // works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}"); const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP); assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found"); 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" // works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects( await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"), 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" // works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects( await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"), testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),

View File

@ -1,8 +1,8 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { MSSQLServerContainer } = require("@testcontainers/mssqlserver"); const { MSSQLServerContainer } = require("@testcontainers/mssqlserver");
const { MssqlMonitorType } = require("../../server/monitor-types/mssql"); const { MssqlMonitorType } = require("../../../server/monitor-types/mssql");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
/** /**
* Helper function to create and start a MSSQL container * Helper function to create and start a MSSQL container
@ -26,7 +26,7 @@ describe(
(process.platform !== "linux" || process.arch !== "x64"), (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; let mssqlContainer;
try { 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 mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType(); 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 mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType(); 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 mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType(); 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 mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType(); 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 mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType(); 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 mssqlContainer = await createAndStartMSSQLContainer();
const mssqlMonitor = new MssqlMonitorType(); 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 mssqlMonitor = new MssqlMonitorType();
const monitor = { const monitor = {
databaseConnectionString: databaseConnectionString:

View File

@ -1,8 +1,8 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { PostgreSqlContainer } = require("@testcontainers/postgresql"); const { PostgreSqlContainer } = require("@testcontainers/postgresql");
const { PostgresMonitorType } = require("../../server/monitor-types/postgres"); const { PostgresMonitorType } = require("../../../server/monitor-types/postgres");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
describe( describe(
"Postgres Single Node", "Postgres Single Node",
@ -12,7 +12,7 @@ describe(
(process.platform !== "linux" || process.arch !== "x64"), (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 // The default timeout of 30 seconds might not be enough for the container to start
const postgresContainer = await new PostgreSqlContainer( const postgresContainer = await new PostgreSqlContainer(
"postgres:latest" "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 postgresMonitor = new PostgresMonitorType();
const monitor = { const monitor = {
databaseConnectionString: "http://localhost:15432", databaseConnectionString: "http://localhost:15432",

View File

@ -1,13 +1,13 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { RabbitMQContainer } = require("@testcontainers/rabbitmq"); const { RabbitMQContainer } = require("@testcontainers/rabbitmq");
const { RabbitMqMonitorType } = require("../../server/monitor-types/rabbitmq"); const { RabbitMqMonitorType } = require("../../../server/monitor-types/rabbitmq");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
describe("RabbitMQ Single Node", { describe("RabbitMQ Single Node", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), 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 // The default timeout of 30 seconds might not be enough for the container to start
const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start(); const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start();
const rabbitMQMonitor = new RabbitMqMonitorType(); 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 rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = { const monitor = {
rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]), rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]),

View File

@ -1,14 +1,9 @@
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { TCPMonitorType } = require("../../server/monitor-types/tcp"); const { TCPMonitorType } = require("../../../server/monitor-types/tcp");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
const net = require("net"); 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", () => { describe("TCP Monitor", () => {
/** /**
* Creates a TCP server on a specified port * Creates a TCP server on a specified port
@ -29,11 +24,7 @@ describe("TCP Monitor", () => {
}); });
} }
/** test("check() sets status to UP when TCP server is reachable", async () => {
* 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 () => {
const port = 12345; const port = 12345;
const server = await createTCPServer(port); const server = await createTCPServer(port);
@ -59,11 +50,7 @@ describe("TCP Monitor", () => {
} }
}); });
/** test("check() rejects with connection failed when TCP server is not running", async () => {
* 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 () => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@ -83,11 +70,7 @@ describe("TCP Monitor", () => {
); );
}); });
/** test("check() rejects when TLS certificate is expired or invalid", async () => {
* 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 => {
const tcpMonitor = new TCPMonitorType(); const tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@ -114,7 +97,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 tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@ -137,7 +120,7 @@ describe("TCP Monitor", () => {
assert.strictEqual(heartbeat.status, UP); 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 tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@ -160,7 +143,7 @@ describe("TCP Monitor", () => {
assert.strictEqual(heartbeat.status, UP); 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 tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {
@ -185,7 +168,7 @@ describe("TCP Monitor", () => {
regex 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 tcpMonitor = new TCPMonitorType();
const monitor = { const monitor = {

View File

@ -1,8 +1,8 @@
const { WebSocketServer } = require("ws"); const { WebSocketServer } = require("ws");
const { describe, test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade"); const { WebSocketMonitorType } = require("../../../server/monitor-types/websocket-upgrade");
const { UP, PENDING } = require("../../src/util"); const { UP, PENDING } = require("../../../src/util");
const net = require("node:net"); const net = require("node:net");
/** /**
@ -22,9 +22,9 @@ function nonCompliantWS(port = 8080) {
return new Promise((resolve) => srv.listen(port, () => resolve(srv))); 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { const monitor = {
@ -68,7 +68,7 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected); 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()); t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType(); const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080 }); const wss = new WebSocketServer({ port: 8080 });
@ -94,7 +94,7 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected); 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { 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()); t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType(); const websocketMonitor = new WebSocketMonitorType();
const wss = await nonCompliantWS(); 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()); t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType(); const websocketMonitor = new WebSocketMonitorType();
const wss = await nonCompliantWS(); const wss = await nonCompliantWS();
@ -185,7 +185,7 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected); 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { const monitor = {
@ -209,7 +209,7 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected); 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { 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 websocketMonitor = new WebSocketMonitorType();
const monitor = { 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()); t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType(); const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080, const wss = new WebSocketServer({ port: 8080,
@ -305,7 +305,7 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected); 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()); t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType(); const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080, const wss = new WebSocketServer({ port: 8080,

View File

@ -1,28 +1,28 @@
const express = require("express"); const express = require("express");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
/** /**
* @param {number} port Port number * @param {number} port Port number
* @param {string} url Webhook URL * @param {string} url Webhook URL
* @param {number} timeout Timeout * @param {number} timeout Timeout
* @returns {Promise<object>} Webhook data * @returns {Promise<object>} Webhook data
*/ */
async function mockWebhook(port, url, timeout = 2500) { async function mockWebhook(port, url, timeout = 2500) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const app = express(); const app = express();
const tmo = setTimeout(() => { const tmo = setTimeout(() => {
server.close(); server.close();
reject({ reason: "Timeout" }); reject({ reason: "Timeout" });
}, timeout); }, timeout);
app.use(bodyParser.json()); // Middleware to parse JSON bodies app.use(bodyParser.json()); // Middleware to parse JSON bodies
app.post(`/${url}`, (req, res) => { app.post(`/${url}`, (req, res) => {
res.status(200).send("OK"); res.status(200).send("OK");
server.close(); server.close();
tmo && clearTimeout(tmo); tmo && clearTimeout(tmo);
resolve(req.body); resolve(req.body);
}); });
const server = app.listen(port); const server = app.listen(port);
}); });
} }
module.exports = mockWebhook; module.exports = mockWebhook;

View File

@ -1,4 +1,4 @@
const { test } = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
@ -36,12 +36,14 @@ iPenGDCg1awOyRnvxNq1MtMDkR9AHwksukzwiYNexYjyvE2t0UzXhFXwazQ3
-----END CERTIFICATE----- -----END CERTIFICATE-----
`; `;
test("Certificate and hostname match", () => { describe("Certificate Hostname Validation", () => {
const result = checkCertificateHostname(testCert, "www.eff.org"); test("checkCertificateHostname() returns true when certificate matches hostname", () => {
assert.strictEqual(result, true); const result = checkCertificateHostname(testCert, "www.eff.org");
}); assert.strictEqual(result, true);
});
test("Certificate and hostname mismatch", () => { test("checkCertificateHostname() returns false when certificate does not match hostname", () => {
const result = checkCertificateHostname(testCert, "example.com"); const result = checkCertificateHostname(testCert, "example.com");
assert.strictEqual(result, false); assert.strictEqual(result, false);
});
}); });

View File

@ -1,9 +1,9 @@
process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(","); 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 assert = require("node:assert");
const DomainExpiry = require("../../server/model/domain_expiry"); 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 TestDB = require("../mock-testdb");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { Notification } = require("../../server/notification"); const { Notification } = require("../../server/notification");
@ -12,30 +12,34 @@ const { setSetting } = require("../../server/util-server");
const testDb = new TestDB(); const testDb = new TestDB();
test("Domain Expiry", async (t) => { describe("Domain Expiry", () => {
await testDb.create();
Notification.init();
const monHttpCom = { const monHttpCom = {
type: "http", type: "http",
url: "https://www.google.com", url: "https://www.google.com",
domainExpiryNotification: true 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"); const d = DomainExpiry.createByName("google.wiki");
assert.deepEqual(await d.getExpiryDate(), new Date("2026-11-26T23:59:59.000Z")); 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 domain = await DomainExpiry.forMonitor(monHttpCom);
const expiryFromRdap = await domain.getExpiryDate(); // from RDAP const expiryFromRdap = await domain.getExpiryDate(); // from RDAP
assert.deepEqual(expiryFromRdap, new Date("2028-09-14T04:00:00.000Z")); 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 await DomainExpiry.checkExpiry(monHttpCom); // RDAP -> Cache
const domain = await DomainExpiry.findByName("google.com"); const domain = await DomainExpiry.findByName("google.com");
assert(Date.now() - domain.lastCheck < 5 * 1000); 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"); await DomainExpiry.findByName("google.com");
const hook = { const hook = {
"port": 3010, "port": 3010,
@ -60,10 +64,10 @@ test("Domain Expiry", async (t) => {
mockWebhook(hook.port, hook.url) mockWebhook(hook.port, hook.url)
]); ]);
assert.match(data.msg, /will expire in/); assert.match(data.msg, /will expire in/);
setTimeout(async () => {
Settings.stopCacheCleaner();
await testDb.destroy();
}, 200);
}); });
}).finally(() => {
setTimeout(async () => {
Settings.stopCacheCleaner();
await testDb.destroy();
}, 200);
}); });

View File

@ -1,65 +0,0 @@
const 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()
*/
test("Maintenance Date Format - MariaDB Compatibility", async (t) => {
await t.test("SQL_DATETIME_FORMAT constant should match MariaDB format", async () => {
assert.strictEqual(SQL_DATETIME_FORMAT, "YYYY-MM-DD HH:mm:ss");
});
await t.test("Format date using SQL_DATETIME_FORMAT", async () => {
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");
});
await t.test("SQL format should not contain ISO markers (T, Z)", async () => {
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'");
});
await t.test("SQL format should match YYYY-MM-DD HH:mm:ss pattern", async () => {
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);
});
await t.test("Parse SQL datetime back to dayjs preserves timestamp", async () => {
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());
});
await t.test("Edge case: midnight timestamp", async () => {
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 () => {
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");
});
});

View File

@ -3,8 +3,8 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { GenericContainer, Wait } = require("testcontainers"); const { GenericContainer, Wait } = require("testcontainers");
describe("Database Migration - Optimize Important Indexes", () => { 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 testDbPath = path.join(__dirname, "../../data/test-migration.db");
const testDbDir = path.dirname(testDbPath); const testDbDir = path.dirname(testDbPath);
@ -57,7 +57,7 @@ describe("Database Migration - Optimize Important Indexes", () => {
}); });
test( test(
"MariaDB: All migrations run successfully", "MariaDB migrations run successfully from fresh database",
{ {
skip: skip:
!!process.env.CI && !!process.env.CI &&

View File

@ -1,4 +1,4 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const { UptimeCalculator } = require("../../server/uptime-calculator"); const { UptimeCalculator } = require("../../server/uptime-calculator");
const dayjs = require("dayjs"); 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("../../server/modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat")); dayjs.extend(require("dayjs/plugin/customParseFormat"));
test("Test Uptime Calculator - custom date", async (t) => { describe("Uptime Calculator", () => {
let c1 = new UptimeCalculator(); test("getCurrentDate() returns custom date when set", () => {
let c1 = new UptimeCalculator();
// Test custom date // Test custom date
UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z"); UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z");
assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix()); assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix());
}); });
test("Test update - UP", async (t) => { test("update() with UP status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(UP); let date = await c2.update(UP);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix());
}); });
test("Test update - MAINTENANCE", async (t) => { test("update() with MAINTENANCE status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(MAINTENANCE); let date = await c2.update(MAINTENANCE);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
}); });
test("Test update - DOWN", async (t) => { test("update() with DOWN status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(DOWN); let date = await c2.update(DOWN);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
}); });
test("Test update - PENDING", async (t) => { test("update() with PENDING status returns correct timestamp", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let date = await c2.update(PENDING); let date = await c2.update(PENDING);
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix()); assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
}); });
test("Test flatStatus", async (t) => { test("flatStatus() converts statuses correctly", () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
assert.strictEqual(c2.flatStatus(UP), UP); assert.strictEqual(c2.flatStatus(UP), UP);
//assert.strictEqual(c2.flatStatus(MAINTENANCE), UP); //assert.strictEqual(c2.flatStatus(MAINTENANCE), UP);
assert.strictEqual(c2.flatStatus(DOWN), DOWN); assert.strictEqual(c2.flatStatus(DOWN), DOWN);
assert.strictEqual(c2.flatStatus(PENDING), DOWN); assert.strictEqual(c2.flatStatus(PENDING), DOWN);
}); });
test("Test getMinutelyKey", async (t) => { test("getMinutelyKey() returns correct timestamp for start of minute", () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00")); let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00"));
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
// Edge case 1 // Edge case 1
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01")); divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01"));
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
// Edge case 2 // Edge case 2
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59")); divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59"));
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix()); assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
}); });
test("Test getDailyKey", async (t) => { test("getDailyKey() returns correct timestamp for start of day", () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00")); let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30")); dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
// Edge case 1 // Edge case 1
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59")); dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
// Edge case 2 // Edge case 2
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00")); dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00"));
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix()); assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
// Test timezone // Test timezone
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)")); 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()); assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix());
}); });
test("Test lastDailyUptimeData", async (t) => { test("lastDailyUptimeData tracks UP status correctly", async () => {
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
assert.strictEqual(c2.lastDailyUptimeData.up, 1); assert.strictEqual(c2.lastDailyUptimeData.up, 1);
}); });
test("Test get24Hour Uptime and Avg Ping", async (t) => { test("get24Hour() calculates uptime and average ping correctly", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// No data // No data
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let data = c2.get24Hour(); let data = c2.get24Hour();
assert.strictEqual(data.uptime, 0); assert.strictEqual(data.uptime, 0);
assert.strictEqual(data.avgPing, null); assert.strictEqual(data.avgPing, null);
// 1 Up // 1 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 100); await c2.update(UP, 100);
let uptime = c2.get24Hour().uptime; let uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
assert.strictEqual(c2.get24Hour().avgPing, 100); assert.strictEqual(c2.get24Hour().avgPing, 100);
// 2 Up // 2 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 100); await c2.update(UP, 100);
await c2.update(UP, 200); await c2.update(UP, 200);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
assert.strictEqual(c2.get24Hour().avgPing, 150); assert.strictEqual(c2.get24Hour().avgPing, 150);
// 3 Up // 3 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 100); await c2.update(UP, 100);
await c2.update(UP, 400); await c2.update(UP, 400);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666); assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666);
// 1 MAINTENANCE // 1 MAINTENANCE
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(MAINTENANCE); await c2.update(MAINTENANCE);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 1 PENDING // 1 PENDING
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(PENDING); await c2.update(PENDING);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 1 DOWN // 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 2 DOWN // 2 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get24Hour().avgPing, null); assert.strictEqual(c2.get24Hour().avgPing, null);
// 1 DOWN, 1 UP // 1 DOWN, 1 UP
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(UP, 0.5); await c2.update(UP, 0.5);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
assert.strictEqual(c2.get24Hour().avgPing, 0.5); assert.strictEqual(c2.get24Hour().avgPing, 0.5);
// 1 UP, 1 DOWN // 1 UP, 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 123); await c2.update(UP, 123);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
assert.strictEqual(c2.get24Hour().avgPing, 123); assert.strictEqual(c2.get24Hour().avgPing, 123);
// Add 24 hours // Add 24 hours
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 0); await c2.update(UP, 0);
await c2.update(UP, 1); await c2.update(UP, 1);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
assert.strictEqual(c2.get24Hour().avgPing, 0.25); 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% // After 24 hours, even if there is no data, the uptime should be still 80%
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
assert.strictEqual(c2.get24Hour().avgPing, 0.25); assert.strictEqual(c2.get24Hour().avgPing, 0.25);
// Add more 24 hours (48 hours) // Add more 24 hours (48 hours)
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour"); UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
// After 48 hours, even if there is no data, the uptime should be still 80% // After 48 hours, even if there is no data, the uptime should be still 80%
uptime = c2.get24Hour().uptime; uptime = c2.get24Hour().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
assert.strictEqual(c2.get24Hour().avgPing, 0.25); assert.strictEqual(c2.get24Hour().avgPing, 0.25);
}); });
test("Test get7DayUptime", async (t) => { test("get7Day() calculates 7-day uptime correctly", async () => {
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
// No data // No data
let c2 = new UptimeCalculator(); let c2 = new UptimeCalculator();
let uptime = c2.get7Day().uptime; let uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 Up // 1 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
// 2 Up // 2 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
// 3 Up // 3 Up
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 1); assert.strictEqual(uptime, 1);
// 1 MAINTENANCE // 1 MAINTENANCE
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(MAINTENANCE); await c2.update(MAINTENANCE);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 PENDING // 1 PENDING
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(PENDING); await c2.update(PENDING);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 DOWN // 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 2 DOWN // 2 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0); assert.strictEqual(uptime, 0);
// 1 DOWN, 1 UP // 1 DOWN, 1 UP
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(DOWN); await c2.update(DOWN);
await c2.update(UP); await c2.update(UP);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
// 1 UP, 1 DOWN // 1 UP, 1 DOWN
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.5); assert.strictEqual(uptime, 0.5);
// Add 7 days // Add 7 days
c2 = new UptimeCalculator(); c2 = new UptimeCalculator();
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(UP); await c2.update(UP);
await c2.update(DOWN); await c2.update(DOWN);
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.8); assert.strictEqual(uptime, 0.8);
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day"); UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day");
// After 7 days, even if there is no data, the uptime should be still 80% // After 7 days, even if there is no data, the uptime should be still 80%
uptime = c2.get7Day().uptime; uptime = c2.get7Day().uptime;
assert.strictEqual(uptime, 0.8); 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) => { let c2 = new UptimeCalculator();
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); let uptime = c2.get30Day().uptime;
assert.strictEqual(uptime, 0);
let c2 = new UptimeCalculator(); let up = 0;
let uptime = c2.get30Day().uptime; let down = 0;
assert.strictEqual(uptime, 0); let flip = true;
for (let i = 0; i < 30; i++) {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
let up = 0; if (flip) {
let down = 0; await c2.update(UP);
let flip = true; up++;
for (let i = 0; i < 30; i++) { } else {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day"); await c2.update(DOWN);
down++;
}
if (flip) { uptime = c2.get30Day().uptime;
await c2.update(UP); assert.strictEqual(uptime, up / (up + down));
up++;
} else { flip = !flip;
await c2.update(DOWN);
down++;
} }
uptime = c2.get30Day().uptime; // Last 7 days
assert.strictEqual(uptime, up / (up + down)); // 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 let c2 = new UptimeCalculator();
// Down, Up, Down, Up, Down, Up, Down let uptime = c2.get1Year().uptime;
// So 3 UP assert.strictEqual(uptime, 0);
assert.strictEqual(c2.get7Day().uptime, 3 / 7);
});
test("Test get1YearUptime (1 check per day)", async (t) => { let flip = true;
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59"); for (let i = 0; i < 365; i++) {
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
let c2 = new UptimeCalculator(); if (flip) {
let uptime = c2.get1Year().uptime; await c2.update(UP);
assert.strictEqual(uptime, 0); } else {
await c2.update(DOWN);
}
let flip = true; uptime = c2.get30Day().time;
for (let i = 0; i < 365; i++) { flip = !flip;
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
if (flip) {
await c2.update(UP);
} else {
await c2.update(DOWN);
} }
uptime = c2.get30Day().time; assert.strictEqual(c2.get1Year().uptime, 183 / 365);
flip = !flip; assert.strictEqual(c2.get30Day().uptime, 15 / 30);
} assert.strictEqual(c2.get7Day().uptime, 4 / 7);
});
assert.strictEqual(c2.get1Year().uptime, 183 / 365); describe("Worst case scenario", () => {
assert.strictEqual(c2.get30Day().uptime, 15 / 30); test("handles year-long simulation with various statuses", {
assert.strictEqual(c2.get7Day().uptime, 4 / 7); 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`, 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));
});
});

View File

@ -1,18 +1,46 @@
const test = require("node:test"); const { describe, test } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const dayjs = require("dayjs");
const { getDaysRemaining, getDaysBetween } = require("../../server/util-server"); const { getDaysRemaining, getDaysBetween } = require("../../server/util-server");
const { SQL_DATETIME_FORMAT } = require("../../src/util");
test("Test getDaysBetween", async (t) => { dayjs.extend(require("dayjs/plugin/utc"));
let days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10)); dayjs.extend(require("dayjs/plugin/customParseFormat"));
assert.strictEqual(days, 3);
days = getDaysBetween(new Date(2024, 9, 7), new Date(2025, 9, 10));
assert.strictEqual(days, 368);
});
test("Test getDaysRemaining", async (t) => { describe("Server Utilities", () => {
let days = getDaysRemaining(new Date(2025, 9, 7), new Date(2025, 9, 10)); test("getDaysBetween() calculates days between dates within same month", () => {
assert.strictEqual(days, 3); const days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10));
days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7)); assert.strictEqual(days, 3);
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);
});
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());
});
}); });