chore: enable formatting over the entire codebase in CI (#6655)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Frank Elsinga
2026-01-09 02:10:36 +01:00
committed by GitHub
parent 6658f2ce41
commit 0f61d7ee1b
422 changed files with 30899 additions and 27379 deletions

View File

@@ -6,8 +6,9 @@ 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
@@ -21,9 +22,9 @@ const { describe, test } = require("node:test");
const assert = require("node:assert");
describe("Feature Name", () => {
test("function() returns expected value when condition is met", () => {
assert.strictEqual(1, 1);
});
test("function() returns expected value when condition is met", () => {
assert.strictEqual(1, 1);
});
});
```

View File

@@ -31,7 +31,7 @@ function getStartEnd(line, key) {
if (start === -1) {
start = 0;
}
return [ start, start + key.length ];
return [start, start + key.length];
}
describe("Check Translations", () => {
@@ -40,14 +40,14 @@ describe("Check Translations", () => {
// this is a resonably crude check, you can get around this trivially
/// this check is just to save on maintainer energy to explain this on every review ^^
const translationRegex = /\$t\(['"](?<key1>.*?)['"]\s*[,)]|i18n-t[^>]*\s+keypath="(?<key2>[^"]+)"/gd;
const translationRegex = /\$t\(['"](?<key1>.*?)['"]\s*[,)]|i18n-t[^>]*\s+keypath="(?<key2>[^"]+)"/dg;
// detect server-side TranslatableError usage: new TranslatableError("key")
const translatableErrorRegex = /new\s+TranslatableError\(\s*['"](?<key3>[^'"]+)['"]\s*\)/g;
const missingKeys = [];
const roots = [ "src", "server" ];
const roots = ["src", "server"];
for (const root of roots) {
for (const filePath of walk(root)) {
@@ -59,7 +59,7 @@ describe("Check Translations", () => {
while ((match = translationRegex.exec(line)) !== null) {
const key = match.groups.key1 || match.groups.key2;
if (key && !enTranslations[key]) {
const [ start, end ] = getStartEnd(line, key);
const [start, end] = getStartEnd(line, key);
missingKeys.push({
filePath,
lineNum: lineNum + 1,
@@ -76,7 +76,7 @@ describe("Check Translations", () => {
while ((m = translatableErrorRegex.exec(line)) !== null) {
const key3 = m.groups.key3;
if (key3 && !enTranslations[key3]) {
const [ start, end ] = getStartEnd(line, key3);
const [start, end] = getStartEnd(line, key3);
missingKeys.push({
filePath,
lineNum: lineNum + 1,
@@ -103,10 +103,11 @@ describe("Check Translations", () => {
report += `\n | ${arrow} unrecognized translation key`;
report += "\n |";
report += `\n = note: please register the translation key '${key}' in en.json so that our awesome team of translators can translate them`;
report += "\n = tip: if you want to contribute translations, please visit https://weblate.kuma.pet\n";
report +=
"\n = tip: if you want to contribute translations, please visit https://weblate.kuma.pet\n";
});
report += "\n===============================";
const fileCount = new Set(missingKeys.map(item => item.filePath)).size;
const fileCount = new Set(missingKeys.map((item) => item.filePath)).size;
report += `\nFound a total of ${missingKeys.length} missing keys in ${fileCount} files.`;
assert.fail(report);
}

View File

@@ -1,6 +1,10 @@
const { describe, test } = require("node:test");
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");
describe("Expression Evaluator", () => {

View File

@@ -6,37 +6,37 @@ test("Test ConditionExpressionGroup.fromMonitor", async (t) => {
const monitor = {
conditions: JSON.stringify([
{
"type": "expression",
"andOr": "and",
"operator": "contains",
"value": "foo",
"variable": "record"
type: "expression",
andOr: "and",
operator: "contains",
value: "foo",
variable: "record",
},
{
"type": "group",
"andOr": "and",
"children": [
type: "group",
andOr: "and",
children: [
{
"type": "expression",
"andOr": "and",
"operator": "contains",
"value": "bar",
"variable": "record"
type: "expression",
andOr: "and",
operator: "contains",
value: "bar",
variable: "record",
},
{
"type": "group",
"andOr": "and",
"children": [
type: "group",
andOr: "and",
children: [
{
"type": "expression",
"andOr": "and",
"operator": "contains",
"value": "car",
"variable": "record"
}
]
type: "expression",
andOr: "and",
operator: "contains",
value: "car",
variable: "record",
},
],
},
]
],
},
]),
};

View File

@@ -1,6 +1,22 @@
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");
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");
describe("Expression Operators", () => {
test("StringEqualsOperator returns true for identical strings and false otherwise", () => {
@@ -25,8 +41,8 @@ describe("Expression Operators", () => {
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"));
assert.strictEqual(true, op.test(["example.org"], "example.org"));
assert.strictEqual(false, op.test(["example.org"], "example.com"));
});
test("NotContainsOperator returns true when scalar does not contain substring", () => {
@@ -37,8 +53,8 @@ describe("Expression Operators", () => {
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"));
assert.strictEqual(true, op.test(["example.org"], "example.com"));
assert.strictEqual(false, op.test(["example.org"], "example.org"));
});
test("StartsWithOperator returns true when string starts with prefix", () => {

View File

@@ -44,10 +44,7 @@ describe("GameDig Monitor", () => {
const gamedigMonitor = new GameDigMonitorType();
mock.method(GameDig, "query", async (options) => {
assert.ok(
net.isIP(options.host) !== 0,
`Expected IP address, got ${options.host}`
);
assert.ok(net.isIP(options.host) !== 0, `Expected IP address, got ${options.host}`);
return {
name: "Test Server",
ping: 50,
@@ -234,10 +231,7 @@ describe("GameDig Monitor", () => {
status: PENDING,
};
await assert.rejects(
gamedigMonitor.check(monitor, heartbeat, {}),
/Error/
);
await assert.rejects(gamedigMonitor.check(monitor, heartbeat, {}), /Error/);
});
test("resolveHostname() returns IP address when given valid hostname", async () => {
@@ -245,10 +239,7 @@ describe("GameDig Monitor", () => {
const resolvedIP = await gamedigMonitor.resolveHostname("localhost");
assert.ok(
net.isIP(resolvedIP) !== 0,
`Expected valid IP address, got ${resolvedIP}`
);
assert.ok(net.isIP(resolvedIP) !== 0, `Expected valid IP address, got ${resolvedIP}`);
});
test("resolveHostname() rejects when DNS resolution fails for invalid hostname", async () => {

View File

@@ -43,7 +43,7 @@ async function createTestGrpcServer(port, methodHandlers) {
longs: String,
enums: String,
defaults: true,
oneofs: true
oneofs: true,
});
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const testPackage = protoDescriptor.test;
@@ -62,245 +62,233 @@ async function createTestGrpcServer(port, methodHandlers) {
});
return new Promise((resolve, reject) => {
server.bindAsync(
`0.0.0.0:${port}`,
grpc.ServerCredentials.createInsecure(),
(err) => {
if (err) {
reject(err);
} else {
server.start();
// Clean up temp file
fs.unlinkSync(protoPath);
resolve(server);
}
server.bindAsync(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) {
reject(err);
} else {
server.start();
// Clean up temp file
fs.unlinkSync(protoPath);
resolve(server);
}
);
});
});
}
describe("GrpcKeywordMonitorType", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
}, () => {
test("check() sets status to UP when keyword is found in response", async () => {
const port = 50051;
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: "Hello World with SUCCESS keyword" });
describe(
"GrpcKeywordMonitorType",
{
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
() => {
test("check() sets status to UP when keyword is found in response", async () => {
const port = 50051;
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: "Hello World with SUCCESS keyword" });
},
});
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "SUCCESS",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await grpcMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.ok(heartbeat.msg.includes("SUCCESS"));
assert.ok(heartbeat.msg.includes("is"));
} finally {
server.forceShutdown();
}
});
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "SUCCESS",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
test("check() rejects when keyword is not found in response", async () => {
const port = 50052;
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: "Hello World without the expected keyword" });
},
});
const heartbeat = {
msg: "",
status: PENDING,
};
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "MISSING",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
try {
await grpcMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.ok(heartbeat.msg.includes("SUCCESS"));
assert.ok(heartbeat.msg.includes("is"));
} finally {
server.forceShutdown();
}
});
const heartbeat = {
msg: "",
status: PENDING,
};
test("check() rejects when keyword is not found in response", async () => {
const port = 50052;
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: "Hello World without the expected keyword" });
}
});
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "MISSING",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(
grpcMonitor.check(monitor, heartbeat, {}),
(err) => {
try {
await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => {
assert.ok(err.message.includes("MISSING"));
assert.ok(err.message.includes("not"));
return true;
}
);
} finally {
server.forceShutdown();
}
});
test("check() rejects when inverted keyword is present in response", async () => {
const port = 50053;
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: "Response with ERROR keyword" });
});
} finally {
server.forceShutdown();
}
});
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "ERROR",
invertKeyword: true,
grpcEnableTls: false,
isInvertKeyword: () => true,
};
test("check() rejects when inverted keyword is present in response", async () => {
const port = 50053;
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: "Response with ERROR keyword" });
},
});
const heartbeat = {
msg: "",
status: PENDING,
};
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "ERROR",
invertKeyword: true,
grpcEnableTls: false,
isInvertKeyword: () => true,
};
try {
await assert.rejects(
grpcMonitor.check(monitor, heartbeat, {}),
(err) => {
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => {
assert.ok(err.message.includes("ERROR"));
assert.ok(err.message.includes("present"));
return true;
}
);
} finally {
server.forceShutdown();
}
});
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) => {
callback(null, { message: "Response without error keyword" });
});
} finally {
server.forceShutdown();
}
});
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "ERROR",
invertKeyword: true,
grpcEnableTls: false,
isInvertKeyword: () => true,
};
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) => {
callback(null, { message: "Response without error keyword" });
},
});
const heartbeat = {
msg: "",
status: PENDING,
};
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "ERROR",
invertKeyword: true,
grpcEnableTls: false,
isInvertKeyword: () => true,
};
try {
await grpcMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.ok(heartbeat.msg.includes("ERROR"));
assert.ok(heartbeat.msg.includes("not"));
} finally {
server.forceShutdown();
}
});
const heartbeat = {
msg: "",
status: PENDING,
};
test("check() rejects when gRPC server is unreachable", async () => {
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: "localhost:50099",
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "SUCCESS",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
try {
await grpcMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.ok(heartbeat.msg.includes("ERROR"));
assert.ok(heartbeat.msg.includes("not"));
} finally {
server.forceShutdown();
}
});
const heartbeat = {
msg: "",
status: PENDING,
};
test("check() rejects when gRPC server is unreachable", async () => {
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: "localhost:50099",
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "SUCCESS",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
await assert.rejects(
grpcMonitor.check(monitor, heartbeat, {}),
(err) => {
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => {
// Should fail with connection error
return true;
}
);
});
test("check() truncates long response messages in error output", async () => {
const port = 50055;
const longMessage = "A".repeat(100) + " with SUCCESS keyword";
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: longMessage });
}
});
});
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "MISSING",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
test("check() truncates long response messages in error output", async () => {
const port = 50055;
const longMessage = "A".repeat(100) + " with SUCCESS keyword";
const heartbeat = {
msg: "",
status: PENDING,
};
const server = await createTestGrpcServer(port, {
Echo: (call, callback) => {
callback(null, { message: longMessage });
},
});
try {
await assert.rejects(
grpcMonitor.check(monitor, heartbeat, {}),
(err) => {
const grpcMonitor = new GrpcKeywordMonitorType();
const monitor = {
grpcUrl: `localhost:${port}`,
grpcProtobuf: testProto,
grpcServiceName: "test.TestService",
grpcMethod: "echo",
grpcBody: JSON.stringify({ message: "test" }),
keyword: "MISSING",
invertKeyword: false,
grpcEnableTls: false,
isInvertKeyword: () => false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => {
// Should truncate message to 50 characters with "..."
assert.ok(err.message.includes("..."));
return true;
}
);
} finally {
server.forceShutdown();
}
});
});
});
} finally {
server.forceShutdown();
}
});
}
);

View File

@@ -15,7 +15,14 @@ const { UP, PENDING } = require("../../../src/util");
* @param {string|null} conditions JSON string of conditions or null
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, monitorTopic = "test", publishTopic = "test", conditions = null) {
async function testMqtt(
mqttSuccessMessage,
mqttCheckType,
receivedMessage,
monitorTopic = "test",
publishTopic = "test",
conditions = null
) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
@@ -56,170 +63,174 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage, moni
return heartbeat;
}
describe("MqttMonitorType", {
concurrency: 4,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => {
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 <-");
});
describe(
"MqttMonitorType",
{
concurrency: 4,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
() => {
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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]"),
);
});
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", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]")
);
});
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\"}"),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
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"}'),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
// Conditions system tests
test("check() sets status to UP when message condition matches (contains)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "message",
operator: "contains",
value: "KEYWORD"
}
]);
const heartbeat = await testMqtt("", null, "-> KEYWORD <-", "test", "test", conditions);
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
// Conditions system tests
test("check() sets status to UP when message condition matches (contains)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "message",
operator: "contains",
value: "KEYWORD",
},
]);
const heartbeat = await testMqtt("", null, "-> KEYWORD <-", "test", "test", conditions);
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("check() sets status to UP when topic condition matches (equals)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "topic",
operator: "equals",
value: "sensors/temp"
}
]);
const heartbeat = await testMqtt("", null, "any message", "sensors/temp", "sensors/temp", conditions);
assert.strictEqual(heartbeat.status, UP);
});
test("check() sets status to UP when topic condition matches (equals)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "topic",
operator: "equals",
value: "sensors/temp",
},
]);
const heartbeat = await testMqtt("", null, "any message", "sensors/temp", "sensors/temp", conditions);
assert.strictEqual(heartbeat.status, UP);
});
test("check() rejects when message condition does not match", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "message",
operator: "contains",
value: "EXPECTED"
}
]);
await assert.rejects(
testMqtt("", null, "actual message without keyword", "test", "test", conditions),
new Error("Conditions not met - Topic: test; Message: actual message without keyword")
);
});
test("check() rejects when message condition does not match", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "message",
operator: "contains",
value: "EXPECTED",
},
]);
await assert.rejects(
testMqtt("", null, "actual message without keyword", "test", "test", conditions),
new Error("Conditions not met - Topic: test; Message: actual message without keyword")
);
});
test("check() sets status to UP with multiple conditions (AND)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "topic",
operator: "equals",
value: "test"
},
{
type: "expression",
variable: "message",
operator: "contains",
value: "success",
andOr: "and"
}
]);
const heartbeat = await testMqtt("", null, "operation success", "test", "test", conditions);
assert.strictEqual(heartbeat.status, UP);
});
});
test("check() sets status to UP with multiple conditions (AND)", async () => {
const conditions = JSON.stringify([
{
type: "expression",
variable: "topic",
operator: "equals",
value: "test",
},
{
type: "expression",
variable: "message",
operator: "contains",
value: "success",
andOr: "and",
},
]);
const heartbeat = await testMqtt("", null, "operation success", "test", "test", conditions);
assert.strictEqual(heartbeat.status, UP);
});
}
);

View File

@@ -9,9 +9,7 @@ const { UP, PENDING } = require("../../../src/util");
* @returns {Promise<{container: MSSQLServerContainer, connectionString: string}>} The started container and connection string
*/
async function createAndStartMSSQLContainer() {
const container = await new MSSQLServerContainer(
"mcr.microsoft.com/mssql/server:2022-latest"
)
const container = await new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-latest")
.acceptLicense()
// The default timeout of 30 seconds might not be enough for the container to start
.withStartupTimeout(60000)
@@ -19,16 +17,14 @@ async function createAndStartMSSQLContainer() {
return {
container,
connectionString: container.getConnectionUri(false)
connectionString: container.getConnectionUri(false),
};
}
describe(
"MSSQL Monitor",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
() => {
test("check() sets status to UP when MSSQL server is reachable", async () => {
@@ -47,11 +43,7 @@ describe(
try {
await mssqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -76,11 +68,7 @@ describe(
"Database connection/query failed: Failed to connect to localhost:15433 - Could not connect (sequence)"
)
);
assert.notStrictEqual(
heartbeat.status,
UP,
`Expected status should not be ${heartbeat.status}`
);
assert.notStrictEqual(heartbeat.status, UP, `Expected status should not be ${heartbeat.status}`);
});
test("check() sets status to UP when custom query returns single value", async () => {
@@ -100,11 +88,7 @@ describe(
try {
await mssqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -135,11 +119,7 @@ describe(
try {
await mssqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -171,15 +151,9 @@ describe(
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Query result did not meet the specified conditions (99)"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
new Error("Query result did not meet the specified conditions (99)")
);
assert.strictEqual(heartbeat.status, PENDING, `Expected status should not be ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -211,15 +185,9 @@ describe(
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Query returned no results"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
new Error("Database connection/query failed: Query returned no results")
);
assert.strictEqual(heartbeat.status, PENDING, `Expected status should not be ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -251,15 +219,9 @@ describe(
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Multiple values were found, expected only one value"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
new Error("Database connection/query failed: Multiple values were found, expected only one value")
);
assert.strictEqual(heartbeat.status, PENDING, `Expected status should not be ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -291,15 +253,9 @@ describe(
try {
await assert.rejects(
mssqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Database connection/query failed: Multiple columns were found, expected only one value"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
new Error("Database connection/query failed: Multiple columns were found, expected only one value")
);
assert.strictEqual(heartbeat.status, PENDING, `Expected status should not be ${heartbeat.status}`);
} finally {
await container.stop();
}

View File

@@ -9,24 +9,20 @@ const { UP, PENDING } = require("../../../src/util");
* @returns {Promise<{container: MariaDbContainer, connectionString: string}>} The started container and connection string
*/
async function createAndStartMariaDBContainer() {
const container = await new MariaDbContainer("mariadb:10.11")
.withStartupTimeout(90000)
.start();
const container = await new MariaDbContainer("mariadb:10.11").withStartupTimeout(90000).start();
const connectionString = `mysql://${container.getUsername()}:${container.getUserPassword()}@${container.getHost()}:${container.getPort()}/${container.getDatabase()}`;
return {
container,
connectionString
connectionString,
};
}
describe(
"MySQL/MariaDB Monitor",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
() => {
test("check() sets status to UP when MariaDB server is reachable", async () => {
@@ -45,11 +41,7 @@ describe(
try {
await mysqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -58,8 +50,7 @@ describe(
test("check() rejects when MariaDB server is not reachable", async () => {
const mysqlMonitor = new MysqlMonitorType();
const monitor = {
databaseConnectionString:
"mysql://invalid:invalid@localhost:13306/test",
databaseConnectionString: "mysql://invalid:invalid@localhost:13306/test",
conditions: "[]",
};
@@ -68,21 +59,14 @@ describe(
status: PENDING,
};
await assert.rejects(
mysqlMonitor.check(monitor, heartbeat, {}),
(err) => {
assert.ok(
err.message.includes("Database connection/query failed"),
`Expected error message to include "Database connection/query failed" but got: ${err.message}`
);
return true;
}
);
assert.notStrictEqual(
heartbeat.status,
UP,
`Expected status should not be ${UP}`
);
await assert.rejects(mysqlMonitor.check(monitor, heartbeat, {}), (err) => {
assert.ok(
err.message.includes("Database connection/query failed"),
`Expected error message to include "Database connection/query failed" but got: ${err.message}`
);
return true;
});
assert.notStrictEqual(heartbeat.status, UP, `Expected status should not be ${UP}`);
});
test("check() sets status to UP when custom query result meets condition", async () => {
@@ -110,11 +94,7 @@ describe(
try {
await mysqlMonitor.check(monitor, heartbeat, {});
assert.strictEqual(
heartbeat.status,
UP,
`Expected status ${UP} but got ${heartbeat.status}`
);
assert.strictEqual(heartbeat.status, UP, `Expected status ${UP} but got ${heartbeat.status}`);
} finally {
await container.stop();
}
@@ -146,15 +126,9 @@ describe(
try {
await assert.rejects(
mysqlMonitor.check(monitor, heartbeat, {}),
new Error(
"Query result did not meet the specified conditions (99)"
)
);
assert.strictEqual(
heartbeat.status,
PENDING,
`Expected status should not be ${heartbeat.status}`
new Error("Query result did not meet the specified conditions (99)")
);
assert.strictEqual(heartbeat.status, PENDING, `Expected status should not be ${heartbeat.status}`);
} finally {
await container.stop();
}

View File

@@ -7,16 +7,12 @@ const { UP, PENDING } = require("../../../src/util");
describe(
"Postgres Single Node",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
() => {
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"
)
const postgresContainer = await new PostgreSqlContainer("postgres:latest")
.withStartupTimeout(60000)
.start();
const postgresMonitor = new PostgresMonitorType();
@@ -51,10 +47,7 @@ describe(
// regex match any string
const regex = /.+/;
await assert.rejects(
postgresMonitor.check(monitor, heartbeat, {}),
regex
);
await assert.rejects(postgresMonitor.check(monitor, heartbeat, {}), regex);
});
}
);

View File

@@ -4,101 +4,99 @@ const { RabbitMQContainer } = require("@testcontainers/rabbitmq");
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"),
}, () => {
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();
const connectionString = `http://${rabbitMQContainer.getHost()}:${rabbitMQContainer.getMappedPort(15672)}`;
describe(
"RabbitMQ Single Node",
{
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
() => {
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();
const connectionString = `http://${rabbitMQContainer.getHost()}:${rabbitMQContainer.getMappedPort(15672)}`;
const monitor = {
rabbitmqNodes: JSON.stringify([ connectionString ]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
};
const monitor = {
rabbitmqNodes: JSON.stringify([connectionString]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await rabbitMQMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Node is reachable and there are no alerts in the cluster");
} finally {
rabbitMQContainer.stop();
}
});
try {
await rabbitMQMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Node is reachable and there are no alerts in the cluster");
} finally {
rabbitMQContainer.stop();
}
});
test("check() rejects when RabbitMQ server is not reachable", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]),
rabbitmqUsername: "rabbitmqUser",
rabbitmqPassword: "rabbitmqPass",
timeout: 10,
};
test("check() rejects when RabbitMQ server is not reachable", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
rabbitmqNodes: JSON.stringify(["http://localhost:15672"]),
rabbitmqUsername: "rabbitmqUser",
rabbitmqPassword: "rabbitmqPass",
timeout: 10,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const heartbeat = {
msg: "",
status: PENDING,
};
// regex match any string
const regex = /.+/;
// regex match any string
const regex = /.+/;
await assert.rejects(
rabbitMQMonitor.check(monitor, heartbeat, {}),
regex
);
});
await assert.rejects(rabbitMQMonitor.check(monitor, heartbeat, {}), regex);
});
test("checkSingleNode() succeeds when node is healthy", async () => {
const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start();
const rabbitMQMonitor = new RabbitMqMonitorType();
const connectionString = `http://${rabbitMQContainer.getHost()}:${rabbitMQContainer.getMappedPort(15672)}`;
test("checkSingleNode() succeeds when node is healthy", async () => {
const rabbitMQContainer = await new RabbitMQContainer().withStartupTimeout(60000).start();
const rabbitMQMonitor = new RabbitMqMonitorType();
const connectionString = `http://${rabbitMQContainer.getHost()}:${rabbitMQContainer.getMappedPort(15672)}`;
const monitor = {
name: "Test Monitor",
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
};
const monitor = {
name: "Test Monitor",
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
};
try {
// Should not throw - just validates the node is healthy
await rabbitMQMonitor.checkSingleNode(monitor, connectionString, "1/1");
} finally {
rabbitMQContainer.stop();
}
});
try {
// Should not throw - just validates the node is healthy
await rabbitMQMonitor.checkSingleNode(monitor, connectionString, "1/1");
} finally {
rabbitMQContainer.stop();
}
});
test("checkSingleNode() throws error when node is unreachable", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
name: "Test Monitor",
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
};
test("checkSingleNode() throws error when node is unreachable", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
name: "Test Monitor",
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
};
// Should reject with any error (connection refused, timeout, etc.)
await assert.rejects(
rabbitMQMonitor.checkSingleNode(monitor, "http://localhost:15672", "1/1"),
Error
);
});
});
// Should reject with any error (connection refused, timeout, etc.)
await assert.rejects(rabbitMQMonitor.checkSingleNode(monitor, "http://localhost:15672", "1/1"), Error);
});
}
);
describe("RabbitMQ Multi-Node (Mocked)", () => {
test("check() succeeds when first node is healthy", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
rabbitmqNodes: JSON.stringify([ "http://node1:15672", "http://node2:15672" ]),
rabbitmqNodes: JSON.stringify(["http://node1:15672", "http://node2:15672"]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
@@ -125,7 +123,7 @@ describe("RabbitMQ Multi-Node (Mocked)", () => {
test("check() succeeds when second node is healthy after first fails", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
rabbitmqNodes: JSON.stringify([ "http://node1:15672", "http://node2:15672" ]),
rabbitmqNodes: JSON.stringify(["http://node1:15672", "http://node2:15672"]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
@@ -155,11 +153,7 @@ describe("RabbitMQ Multi-Node (Mocked)", () => {
test("check() fails with consolidated error when all nodes are down", async () => {
const rabbitMQMonitor = new RabbitMqMonitorType();
const monitor = {
rabbitmqNodes: JSON.stringify([
"http://node1:15672",
"http://node2:15672",
"http://node3:15672"
]),
rabbitmqNodes: JSON.stringify(["http://node1:15672", "http://node2:15672", "http://node3:15672"]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
timeout: 10,
@@ -177,16 +171,13 @@ describe("RabbitMQ Multi-Node (Mocked)", () => {
throw new Error(`Connection failed to node ${callCount}`);
};
await assert.rejects(
rabbitMQMonitor.check(monitor, heartbeat, {}),
(error) => {
assert.match(error.message, /All 3 nodes failed/);
assert.match(error.message, /Node 1:/);
assert.match(error.message, /Node 2:/);
assert.match(error.message, /Node 3:/);
return true;
}
);
await assert.rejects(rabbitMQMonitor.check(monitor, heartbeat, {}), (error) => {
assert.match(error.message, /All 3 nodes failed/);
assert.match(error.message, /Node 1:/);
assert.match(error.message, /Node 2:/);
assert.match(error.message, /Node 3:/);
return true;
});
assert.strictEqual(callCount, 3, "Should check all three nodes");
});
@@ -204,10 +195,7 @@ describe("RabbitMQ Multi-Node (Mocked)", () => {
status: PENDING,
};
await assert.rejects(
rabbitMQMonitor.check(monitor, heartbeat, {}),
/No RabbitMQ nodes configured/
);
await assert.rejects(rabbitMQMonitor.check(monitor, heartbeat, {}), /No RabbitMQ nodes configured/);
});
test("check() tries all nodes before failing", async () => {
@@ -217,7 +205,7 @@ describe("RabbitMQ Multi-Node (Mocked)", () => {
"http://node1:15672",
"http://node2:15672",
"http://node3:15672",
"http://node4:15672"
"http://node4:15672",
]),
rabbitmqUsername: "guest",
rabbitmqPassword: "guest",
@@ -235,11 +223,8 @@ describe("RabbitMQ Multi-Node (Mocked)", () => {
throw new Error(`Failed: ${url}`);
};
await assert.rejects(
rabbitMQMonitor.check(monitor, heartbeat, {}),
/All 4 nodes failed/
);
await assert.rejects(rabbitMQMonitor.check(monitor, heartbeat, {}), /All 4 nodes failed/);
assert.strictEqual(checkedNodes.length, 4, "Should check all 4 nodes");
assert.strictEqual(checkedNodes[0], "http://node1:15672");
assert.strictEqual(checkedNodes[1], "http://node2:15672");

View File

@@ -25,7 +25,7 @@ describe("TCP Monitor", () => {
heartbeat.status = PENDING;
// Wait a bit before retrying with exponential backoff
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
}
}
}
@@ -45,7 +45,7 @@ describe("TCP Monitor", () => {
resolve(server);
});
server.on("error", err => {
server.on("error", (err) => {
reject(err);
});
});
@@ -91,10 +91,7 @@ describe("TCP Monitor", () => {
status: PENDING,
};
await assert.rejects(
tcpMonitor.check(monitor, heartbeat, {}),
new Error("Connection failed")
);
await assert.rejects(tcpMonitor.check(monitor, heartbeat, {}), new Error("Connection failed"));
});
test("check() rejects when TLS certificate is expired or invalid", async () => {
@@ -105,7 +102,7 @@ describe("TCP Monitor", () => {
port: 443,
smtpSecurity: "secure",
isEnabledExpiryNotification: () => true,
handleTlsInfo: async tlsInfo => {
handleTlsInfo: async (tlsInfo) => {
return tlsInfo;
},
};
@@ -118,10 +115,7 @@ describe("TCP Monitor", () => {
// Regex: contains with "TLS Connection failed:" or "Certificate is invalid"
const regex = /TLS Connection failed:|Certificate is invalid/;
await assert.rejects(
tcpMonitor.check(monitor, heartbeat, {}),
regex
);
await assert.rejects(tcpMonitor.check(monitor, heartbeat, {}), regex);
});
test("check() sets status to UP when TLS certificate is valid (SSL)", async () => {
@@ -132,7 +126,7 @@ describe("TCP Monitor", () => {
port: 465,
smtpSecurity: "secure",
isEnabledExpiryNotification: () => true,
handleTlsInfo: async tlsInfo => {
handleTlsInfo: async (tlsInfo) => {
return tlsInfo;
},
};
@@ -156,7 +150,7 @@ describe("TCP Monitor", () => {
port: 587,
smtpSecurity: "starttls",
isEnabledExpiryNotification: () => true,
handleTlsInfo: async tlsInfo => {
handleTlsInfo: async (tlsInfo) => {
return tlsInfo;
},
};
@@ -180,7 +174,7 @@ describe("TCP Monitor", () => {
port: 587,
smtpSecurity: "starttls",
isEnabledExpiryNotification: () => true,
handleTlsInfo: async tlsInfo => {
handleTlsInfo: async (tlsInfo) => {
return tlsInfo;
},
};
@@ -192,10 +186,7 @@ describe("TCP Monitor", () => {
const regex = /does not match certificate/;
await assert.rejects(
tcpMonitor.check(monitor, heartbeat, {}),
regex
);
await assert.rejects(tcpMonitor.check(monitor, heartbeat, {}), regex);
});
test("check() sets status to UP for XMPP server with valid certificate (STARTTLS)", async () => {
const tcpMonitor = new TCPMonitorType();
@@ -205,7 +196,7 @@ describe("TCP Monitor", () => {
port: 5222,
smtpSecurity: "starttls",
isEnabledExpiryNotification: () => true,
handleTlsInfo: async tlsInfo => {
handleTlsInfo: async (tlsInfo) => {
return tlsInfo;
},
};

View File

@@ -13,16 +13,15 @@ const http = require("node:http");
function nonCompliantWS() {
const srv = net.createServer((socket) => {
socket.once("data", (buf) => {
socket.write("HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n\r\n");
socket.write(
"HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n\r\n"
);
socket.destroy();
});
});
return new Promise((resolve) => {
srv.listen(0, () => {
resolve({ server: srv,
port: srv.address().port });
resolve({ server: srv, port: srv.address().port });
});
});
}
@@ -38,8 +37,7 @@ function httpServer() {
});
return new Promise((resolve) => {
srv.listen(0, () => {
resolve({ server: srv,
port: srv.address().port });
resolve({ server: srv, port: srv.address().port });
});
});
}
@@ -51,17 +49,14 @@ function httpServer() {
*/
function createWebSocketServer(options = {}) {
return new Promise((resolve) => {
const wss = new WebSocketServer({ port: 0,
...options });
const wss = new WebSocketServer({ port: 0, ...options });
wss.on("listening", () => {
resolve({ server: wss,
port: wss.address().port });
resolve({ server: wss, port: wss.address().port });
});
});
}
describe("WebSocket Monitor", {
}, () => {
describe("WebSocket Monitor", {}, () => {
test("check() rejects with unexpected server response when connecting to non-WebSocket server", {}, async (t) => {
const websocketMonitor = new WebSocketMonitorType();
const { server: srv, port } = await httpServer();
@@ -92,7 +87,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -118,7 +113,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -144,7 +139,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1001" ]),
accepted_statuscodes_json: JSON.stringify(["1001"]),
timeout: 30,
};
@@ -153,10 +148,7 @@ describe("WebSocket Monitor", {
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected status code: 1000")
);
await assert.rejects(websocketMonitor.check(monitor, heartbeat, {}), new Error("Unexpected status code: 1000"));
});
test("check() rejects when expected status code is empty", async (t) => {
@@ -167,7 +159,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "" ]),
accepted_statuscodes_json: JSON.stringify([""]),
timeout: 30,
};
@@ -176,10 +168,7 @@ describe("WebSocket Monitor", {
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Unexpected status code: 1000")
);
await assert.rejects(websocketMonitor.check(monitor, heartbeat, {}), new Error("Unexpected status code: 1000"));
});
test("check() rejects when Sec-WebSocket-Accept header is invalid", async (t) => {
@@ -190,7 +179,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -213,7 +202,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: true,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -239,7 +228,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: true,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -265,7 +254,7 @@ describe("WebSocket Monitor", {
const monitor = {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: true,
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -286,7 +275,7 @@ describe("WebSocket Monitor", {
handleProtocols: (protocols) => {
// Explicitly reject all subprotocols
return null;
}
},
});
t.after(() => wss.close());
@@ -294,7 +283,7 @@ describe("WebSocket Monitor", {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "ocpp1.6",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -303,10 +292,7 @@ describe("WebSocket Monitor", {
status: PENDING,
};
await assert.rejects(
websocketMonitor.check(monitor, heartbeat, {}),
new Error("Server sent no subprotocol")
);
await assert.rejects(websocketMonitor.check(monitor, heartbeat, {}), new Error("Server sent no subprotocol"));
});
test("check() rejects when multiple subprotocols contain invalid characters", async (t) => {
@@ -318,7 +304,7 @@ describe("WebSocket Monitor", {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: " # & ,ocpp2.0 [] , ocpp1.6 , ,, ; ",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -338,7 +324,7 @@ describe("WebSocket Monitor", {
const { server: wss, port } = await createWebSocketServer({
handleProtocols: (protocols) => {
return Array.from(protocols).includes("test") ? "test" : null;
}
},
});
t.after(() => wss.close());
@@ -346,7 +332,7 @@ describe("WebSocket Monitor", {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "invalid , test ",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};
@@ -369,7 +355,7 @@ describe("WebSocket Monitor", {
const { server: wss, port } = await createWebSocketServer({
handleProtocols: (protocols) => {
return Array.from(protocols).includes("test") ? "test" : null;
}
},
});
t.after(() => wss.close());
@@ -377,7 +363,7 @@ describe("WebSocket Monitor", {
url: `ws://localhost:${port}`,
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "invalid,test",
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
accepted_statuscodes_json: JSON.stringify(["1000"]),
timeout: 30,
};

View File

@@ -5,7 +5,7 @@ const hash = require("../../../server/modules/axios-ntlm/lib/hash");
describe("createPseudoRandomValue()", () => {
test("returns a hexadecimal string with the requested length", () => {
for (const length of [ 0, 8, 16, 32, 64 ]) {
for (const length of [0, 8, 16, 32, 64]) {
const result = hash.createPseudoRandomValue(length);
assert.strictEqual(typeof result, "string");
assert.strictEqual(result.length, length);

View File

@@ -1,4 +1,4 @@
process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(",");
process.env.UPTIME_KUMA_HIDE_LOG = ["info_db", "info_server"].join(",");
const { describe, test, mock, before, after } = require("node:test");
const assert = require("node:assert");
@@ -16,7 +16,7 @@ describe("Domain Expiry", () => {
const monHttpCom = {
type: "http",
url: "https://www.google.com",
domainExpiryNotification: true
domainExpiryNotification: true,
};
before(async () => {
@@ -39,7 +39,7 @@ describe("Domain Expiry", () => {
const supportInfo = await DomainExpiry.checkSupport(monHttpCom);
let expected = {
domain: "google.com",
tld: "com"
tld: "com",
};
assert.deepStrictEqual(supportInfo, expected);
});
@@ -49,7 +49,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -64,7 +64,7 @@ describe("Domain Expiry", () => {
test("throws error for undefined target", async () => {
const monitor = {
type: "http",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -80,7 +80,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: null,
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -98,7 +98,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -114,7 +114,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://192.168.1.1",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -130,7 +130,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://[2001:db8::1]",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -146,7 +146,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://example.x",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -164,7 +164,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://api.staging.example.com/v1/users",
domainExpiryNotification: true
domainExpiryNotification: true,
};
const supportInfo = await DomainExpiry.checkSupport(monitor);
assert.strictEqual(supportInfo.domain, "example.com");
@@ -175,7 +175,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://mail.subdomain.example.org",
domainExpiryNotification: true
domainExpiryNotification: true,
};
const supportInfo = await DomainExpiry.checkSupport(monitor);
assert.strictEqual(supportInfo.domain, "example.org");
@@ -186,7 +186,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://example.com:8080/api",
domainExpiryNotification: true
domainExpiryNotification: true,
};
const supportInfo = await DomainExpiry.checkSupport(monitor);
assert.strictEqual(supportInfo.domain, "example.com");
@@ -197,7 +197,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://example.com/search?q=test&page=1",
domainExpiryNotification: true
domainExpiryNotification: true,
};
const supportInfo = await DomainExpiry.checkSupport(monitor);
assert.strictEqual(supportInfo.domain, "example.com");
@@ -208,7 +208,7 @@ describe("Domain Expiry", () => {
const monitor = {
type: "http",
url: "https://example.localhost",
domainExpiryNotification: true
domainExpiryNotification: true,
};
await assert.rejects(
async () => await DomainExpiry.checkSupport(monitor),
@@ -237,26 +237,26 @@ describe("Domain Expiry", () => {
test("sendNotifications() triggers notification for expiring domain", async () => {
await DomainExpiry.findByName("google.com");
const hook = {
"port": 3010,
"url": "capture"
port: 3010,
url: "capture",
};
await setSetting("domainExpiryNotifyDays", [ 1, 2, 1500 ], "general");
await setSetting("domainExpiryNotifyDays", [1, 2, 1500], "general");
const notif = R.convertToBean("notification", {
"config": JSON.stringify({
config: JSON.stringify({
type: "webhook",
httpMethod: "post",
webhookContentType: "json",
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`,
}),
"active": 1,
"user_id": 1,
"name": "Testhook"
active: 1,
user_id: 1,
name: "Testhook",
});
const manyDays = 3650;
setSetting("domainExpiryNotifyDays", [ manyDays ], "general");
const [ , data ] = await Promise.all([
DomainExpiry.sendNotifications("google.com", [ notif ]),
mockWebhook(hook.port, hook.url)
setSetting("domainExpiryNotifyDays", [manyDays], "general");
const [, data] = await Promise.all([
DomainExpiry.sendNotifications("google.com", [notif]),
mockWebhook(hook.port, hook.url),
]);
assert.match(data.msg, /will expire in/);
});
@@ -267,15 +267,15 @@ describe("Domain Expiry", () => {
const mockDomain = {
domain: "test-null.com",
expiry: null,
lastExpiryNotificationSent: null
lastExpiryNotificationSent: null,
};
mock.method(DomainExpiry, "findByDomainNameOrCreate", async () => mockDomain);
try {
const hook = {
"port": 3012,
"url": "should-not-be-called-null"
port: 3012,
url: "should-not-be-called-null",
};
const notif = {
@@ -284,22 +284,24 @@ describe("Domain Expiry", () => {
type: "webhook",
httpMethod: "post",
webhookContentType: "json",
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
})
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`,
}),
};
// Race between sendNotifications and mockWebhook timeout
// If webhook is called, we fail. If it times out, we pass.
const result = await Promise.race([
DomainExpiry.sendNotifications("test-null.com", [ notif ]),
mockWebhook(hook.port, hook.url, 500).then(() => {
throw new Error("Webhook was called but should not have been for null expiry");
}).catch((e) => {
if (e.reason === "Timeout") {
return "timeout"; // Expected - webhook was not called
}
throw e;
})
DomainExpiry.sendNotifications("test-null.com", [notif]),
mockWebhook(hook.port, hook.url, 500)
.then(() => {
throw new Error("Webhook was called but should not have been for null expiry");
})
.catch((e) => {
if (e.reason === "Timeout") {
return "timeout"; // Expected - webhook was not called
}
throw e;
}),
]);
assert.ok(result === undefined || result === "timeout", "Should not send notification for null expiry");
@@ -314,14 +316,14 @@ describe("Domain Expiry", () => {
const mockDomain = {
domain: "test-undefined.com",
expiry: undefined,
lastExpiryNotificationSent: null
lastExpiryNotificationSent: null,
};
mock.method(DomainExpiry, "findByDomainNameOrCreate", async () => mockDomain);
const hook = {
"port": 3013,
"url": "should-not-be-called-undefined"
port: 3013,
url: "should-not-be-called-undefined",
};
const notif = {
@@ -330,25 +332,30 @@ describe("Domain Expiry", () => {
type: "webhook",
httpMethod: "post",
webhookContentType: "json",
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
})
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`,
}),
};
// Race between sendNotifications and mockWebhook timeout
// If webhook is called, we fail. If it times out, we pass.
const result = await Promise.race([
DomainExpiry.sendNotifications("test-undefined.com", [ notif ]),
mockWebhook(hook.port, hook.url, 500).then(() => {
throw new Error("Webhook was called but should not have been for undefined expiry");
}).catch((e) => {
if (e.reason === "Timeout") {
return "timeout"; // Expected - webhook was not called
}
throw e;
})
DomainExpiry.sendNotifications("test-undefined.com", [notif]),
mockWebhook(hook.port, hook.url, 500)
.then(() => {
throw new Error("Webhook was called but should not have been for undefined expiry");
})
.catch((e) => {
if (e.reason === "Timeout") {
return "timeout"; // Expected - webhook was not called
}
throw e;
}),
]);
assert.ok(result === undefined || result === "timeout", "Should not send notification for undefined expiry");
assert.ok(
result === undefined || result === "timeout",
"Should not send notification for undefined expiry"
);
} finally {
mock.restoreAll();
}

View File

@@ -27,7 +27,7 @@ describe("Database Migration", () => {
const db = knex({
client: Dialect,
connection: {
filename: testDbPath
filename: testDbPath,
},
useNullAsDefault: true,
});
@@ -43,11 +43,10 @@ describe("Database Migration", () => {
// Run all migrations like production code does
await R.knex.migrate.latest({
directory: path.join(__dirname, "../../db/knex_migrations")
directory: path.join(__dirname, "../../db/knex_migrations"),
});
// Test passes if migrations complete successfully without errors
} finally {
// Clean up
await R.knex.destroy();
@@ -60,18 +59,16 @@ describe("Database Migration", () => {
test(
"MariaDB migrations run successfully from fresh database",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
async () => {
// Start MariaDB container (using MariaDB 12 to match current production)
const mariadbContainer = await new GenericContainer("mariadb:12")
.withEnvironment({
"MYSQL_ROOT_PASSWORD": "root",
"MYSQL_DATABASE": "kuma_test",
"MYSQL_USER": "kuma",
"MYSQL_PASSWORD": "kuma"
MYSQL_ROOT_PASSWORD: "root",
MYSQL_DATABASE: "kuma_test",
MYSQL_USER: "kuma",
MYSQL_PASSWORD: "kuma",
})
.withExposedPorts(3306)
.withWaitStrategy(Wait.forLogMessage("ready for connections", 2))
@@ -79,7 +76,7 @@ describe("Database Migration", () => {
.start();
// Wait a bit more to ensure MariaDB is fully ready
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 2000));
const knex = require("knex");
const knexInstance = knex({
@@ -111,11 +108,10 @@ describe("Database Migration", () => {
// Run all migrations like production code does
await R.knex.migrate.latest({
directory: path.join(__dirname, "../../db/knex_migrations")
directory: path.join(__dirname, "../../db/knex_migrations"),
});
// Test passes if migrations complete successfully without errors
} finally {
// Clean up
try {
@@ -135,15 +131,11 @@ describe("Database Migration", () => {
test(
"MySQL migrations run successfully from fresh database",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
},
async () => {
// Start MySQL 8.0 container (the version mentioned in the issue)
const mysqlContainer = await new MySqlContainer("mysql:8.0")
.withStartupTimeout(120000)
.start();
const mysqlContainer = await new MySqlContainer("mysql:8.0").withStartupTimeout(120000).start();
const knex = require("knex");
const knexInstance = knex({
@@ -175,11 +167,10 @@ describe("Database Migration", () => {
// Run all migrations like production code does
await R.knex.migrate.latest({
directory: path.join(__dirname, "../../db/knex_migrations")
directory: path.join(__dirname, "../../db/knex_migrations"),
});
// Test passes if migrations complete successfully without errors
} finally {
// Clean up
try {

View File

@@ -1,7 +1,12 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const StatusPage = require("../../server/model/status_page");
const { STATUS_PAGE_ALL_UP, STATUS_PAGE_ALL_DOWN, STATUS_PAGE_PARTIAL_DOWN, STATUS_PAGE_MAINTENANCE } = require("../../src/util");
const {
STATUS_PAGE_ALL_UP,
STATUS_PAGE_ALL_DOWN,
STATUS_PAGE_PARTIAL_DOWN,
STATUS_PAGE_MAINTENANCE,
} = require("../../src/util");
describe("StatusPage", () => {
describe("getStatusDescription()", () => {

View File

@@ -21,7 +21,7 @@ function shouldSkip() {
// -> Check if PID 1 is systemd (or init which maps to systemd)
try {
const pid1Comm = execSync("ps -p 1 -o comm=", { encoding: "utf-8" }).trim();
return ![ "systemd", "init" ].includes(pid1Comm);
return !["systemd", "init"].includes(pid1Comm);
} catch (e) {
return true;
}

View File

@@ -348,61 +348,65 @@ describe("Uptime Calculator", () => {
});
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"));
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());
assert.strictEqual(c.minutelyUptimeDataList.length(), 1440);
assert.strictEqual(c.dailyUptimeDataList.length(), 365);
});
let c = new UptimeCalculator();
let up = 0;
let down = 0;
let interval = 20;
await t.test("get1YearUptime()", async () => {
assert.strictEqual(c.get1Year().uptime, up / (up + down));
});
});
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));
});
}
);
});
});
@@ -411,7 +415,7 @@ describe("Uptime Calculator", () => {
* @returns {{rss: string, heapTotal: string, heapUsed: string, external: string}} Current memory usage
*/
function memoryUsage() {
const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;
const formatMemoryUsage = (data) => `${Math.round((data / 1024 / 1024) * 100) / 100} MB`;
const memoryData = process.memoryUsage();
return {

View File

@@ -2,7 +2,6 @@ import { expect, test } from "@playwright/test";
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
test.describe("Example Spec", () => {
test.beforeEach(async ({ page }) => {
await restoreSqliteSnapshot(page);
});
@@ -35,5 +34,4 @@ test.describe("Example Spec", () => {
await expect(page.getByTestId("monitor-list")).not.toContainText("example.com");
await screenshot(testInfo, page);
});
});

View File

@@ -2,7 +2,6 @@ import { test } from "@playwright/test";
import { getSqliteDatabaseExists, login, screenshot, takeSqliteSnapshot } from "../util-test";
test.describe("Uptime Kuma Setup", () => {
test.skip(() => getSqliteDatabaseExists(), "Must only run once per session");
test.afterEach(async ({ page }, testInfo) => {
@@ -51,5 +50,4 @@ test.describe("Uptime Kuma Setup", () => {
test("take sqlite snapshot", async ({ page }) => {
await takeSqliteSnapshot(page);
});
});

View File

@@ -2,7 +2,6 @@ import { expect, test } from "@playwright/test";
import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
test.describe("Status Page", () => {
test.beforeEach(async ({ page }) => {
await restoreSqliteSnapshot(page);
});
@@ -127,16 +126,21 @@ test.describe("Status Page", () => {
await expect(page.getByTestId("monitor-name")).toHaveAttribute("href", monitorCustomUrl);
await expect(page.getByTestId("update-countdown-text")).toContainText("00:");
const updateCountdown = Number((await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]);
const updateCountdown = Number(
(await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]
);
expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval - 10); // cant be certain when the timer will start, so ensure it's within expected range
expect(updateCountdown).toBeLessThanOrEqual(refreshInterval);
await expect(page.locator("body")).toHaveClass(theme);
// Add Google Analytics ID to head and verify
await page.waitForFunction(() => {
return document.head.innerHTML.includes("https://www.googletagmanager.com/gtag/js?id=");
}, { timeout: 5000 });
await page.waitForFunction(
() => {
return document.head.innerHTML.includes("https://www.googletagmanager.com/gtag/js?id=");
},
{ timeout: 5000 }
);
expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId);
const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor);
@@ -178,9 +182,13 @@ test.describe("Status Page", () => {
await page.getByTestId("analytics-id-input").fill(plausibleAnalyticsDomainsUrls);
await page.getByTestId("save-button").click();
await screenshot(testInfo, page);
await page.waitForFunction((scriptUrl) => {
return document.head.innerHTML.includes(scriptUrl);
}, plausibleAnalyticsScriptUrl, { timeout: 5000 });
await page.waitForFunction(
(scriptUrl) => {
return document.head.innerHTML.includes(scriptUrl);
},
plausibleAnalyticsScriptUrl,
{ timeout: 5000 }
);
expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsScriptUrl);
expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsDomainsUrls);
@@ -191,9 +199,13 @@ test.describe("Status Page", () => {
await page.getByTestId("analytics-id-input").fill(matomoSiteId);
await page.getByTestId("save-button").click();
await screenshot(testInfo, page);
await page.waitForFunction((url) => {
return document.head.innerHTML.includes(url);
}, matomoUrl, { timeout: 5000 });
await page.waitForFunction(
(url) => {
return document.head.innerHTML.includes(url);
},
matomoUrl,
{ timeout: 5000 }
);
expect(await page.locator("head").innerHTML()).toContain(matomoUrl);
expect(await page.locator("head").innerHTML()).toContain(matomoSiteId);
});
@@ -269,7 +281,7 @@ test.describe("Status Page", () => {
// Attach RSS content for inspection
await testInfo.attach("rss-feed.xml", {
body: rssContent,
contentType: "application/xml"
contentType: "application/xml",
});
// Verify all payloads are escaped using CDATA
@@ -278,7 +290,7 @@ test.describe("Status Page", () => {
expect(rssContent).toContain(`<title><![CDATA[${normalMonitorName} is down]]></title>`);
// Verify RSS feed structure is valid
expect(rssContent).toContain("<?xml version=\"1.0\"");
expect(rssContent).toContain('<?xml version="1.0"');
expect(rssContent).toContain("<rss");
expect(rssContent).toContain("</rss>");
@@ -306,10 +318,9 @@ test.describe("Status Page", () => {
await testInfo.attach("rss-feed-custom-title.xml", {
body: rssContentCustom,
contentType: "application/xml"
contentType: "application/xml",
});
await screenshot(testInfo, page);
});
});

View File

@@ -13,7 +13,7 @@ export async function screenshot(testInfo, page) {
const screenshot = await page.screenshot();
await testInfo.attach("screenshot", {
body: screenshot,
contentType: "image/png"
contentType: "image/png",
});
}

View File

@@ -1,27 +1,27 @@
const { sync: rimrafSync } = require("rimraf");
const Database = require("../server/database");
class TestDB {
dataDir;
constructor(dir = "./data/test") {
this.dataDir = dir;
}
async create() {
Database.initDataDir({ "data-dir": this.dataDir });
Database.dbConfig = {
type: "sqlite"
};
Database.writeDBConfig(Database.dbConfig);
await Database.connect(true);
await Database.patch();
}
async destroy() {
await Database.close();
this.dataDir && rimrafSync(this.dataDir);
}
}
module.exports = TestDB;
const { sync: rimrafSync } = require("rimraf");
const Database = require("../server/database");
class TestDB {
dataDir;
constructor(dir = "./data/test") {
this.dataDir = dir;
}
async create() {
Database.initDataDir({ "data-dir": this.dataDir });
Database.dbConfig = {
type: "sqlite",
};
Database.writeDBConfig(Database.dbConfig);
await Database.connect(true);
await Database.patch();
}
async destroy() {
await Database.close();
this.dataDir && rimrafSync(this.dataDir);
}
}
module.exports = TestDB;