summaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-11-26 09:28:28 +0100
committerDaniel Baumann <daniel@debian.org>2024-11-26 12:25:58 +0100
commita1882b67c41fe9901a0cd8059b5cc78a5beadec0 (patch)
tree2a24507c67aa99a15416707b2f7e645142230ed8 /test
parentInitial commit. (diff)
downloaduptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.tar.xz
uptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.zip
Adding upstream version 2.0.0~beta.0+dfsg.upstream/2.0.0_beta.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'test')
-rw-r--r--test/backend-test/README.md22
-rw-r--r--test/backend-test/monitor-conditions/test-evaluator.js46
-rw-r--r--test/backend-test/monitor-conditions/test-expressions.js55
-rw-r--r--test/backend-test/monitor-conditions/test-operators.js108
-rw-r--r--test/backend-test/test-mqtt.js102
-rw-r--r--test/backend-test/test-rabbitmq.js53
-rw-r--r--test/backend-test/test-uptime-calculator.js425
-rw-r--r--test/e2e/specs/example.spec.js39
-rw-r--r--test/e2e/specs/monitor-form.spec.js104
-rw-r--r--test/e2e/specs/setup-process.once.js55
-rw-r--r--test/e2e/specs/status-page.spec.js129
-rw-r--r--test/e2e/util-test.js62
-rw-r--r--test/prepare-test-server.js10
-rw-r--r--test/test-radius.dockerfile13
14 files changed, 1223 insertions, 0 deletions
diff --git a/test/backend-test/README.md b/test/backend-test/README.md
new file mode 100644
index 0000000..775ffb7
--- /dev/null
+++ b/test/backend-test/README.md
@@ -0,0 +1,22 @@
+# Node.js Test Runner
+
+Documentation: https://nodejs.org/api/test.html
+
+Create a test file in this directory with the name `*.js`.
+
+## Template
+
+```js
+const test = require("node:test");
+const assert = require("node:assert");
+
+test("Test name", async (t) => {
+ assert.strictEqual(1, 1);
+});
+```
+
+## Run
+
+```bash
+npm run test-backend
+```
diff --git a/test/backend-test/monitor-conditions/test-evaluator.js b/test/backend-test/monitor-conditions/test-evaluator.js
new file mode 100644
index 0000000..da7c7fa
--- /dev/null
+++ b/test/backend-test/monitor-conditions/test-evaluator.js
@@ -0,0 +1,46 @@
+const test = require("node:test");
+const assert = require("node:assert");
+const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("../../../server/monitor-conditions/expression.js");
+const { evaluateExpressionGroup, evaluateExpression } = require("../../../server/monitor-conditions/evaluator.js");
+
+test("Test evaluateExpression", async (t) => {
+ const expr = new ConditionExpression("record", "contains", "mx1.example.com");
+ assert.strictEqual(true, evaluateExpression(expr, { record: "mx1.example.com" }));
+ assert.strictEqual(false, evaluateExpression(expr, { record: "mx2.example.com" }));
+});
+
+test("Test evaluateExpressionGroup with logical AND", async (t) => {
+ const group = new ConditionExpressionGroup([
+ new ConditionExpression("record", "contains", "mx1."),
+ new ConditionExpression("record", "contains", "example.com", LOGICAL.AND),
+ ]);
+ assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
+});
+
+test("Test evaluateExpressionGroup with logical OR", async (t) => {
+ const group = new ConditionExpressionGroup([
+ new ConditionExpression("record", "contains", "example.com"),
+ new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
+ ]);
+ assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.com" }));
+ assert.strictEqual(true, evaluateExpressionGroup(group, { record: "example.org" }));
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.net" }));
+});
+
+test("Test evaluateExpressionGroup with nested group", async (t) => {
+ const group = new ConditionExpressionGroup([
+ new ConditionExpression("record", "contains", "mx1."),
+ new ConditionExpressionGroup([
+ new ConditionExpression("record", "contains", "example.com"),
+ new ConditionExpression("record", "contains", "example.org", LOGICAL.OR),
+ ]),
+ ]);
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1." }));
+ assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.com" }));
+ assert.strictEqual(true, evaluateExpressionGroup(group, { record: "mx1.example.org" }));
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.com" }));
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "example.org" }));
+ assert.strictEqual(false, evaluateExpressionGroup(group, { record: "mx1.example.net" }));
+});
diff --git a/test/backend-test/monitor-conditions/test-expressions.js b/test/backend-test/monitor-conditions/test-expressions.js
new file mode 100644
index 0000000..fc723a2
--- /dev/null
+++ b/test/backend-test/monitor-conditions/test-expressions.js
@@ -0,0 +1,55 @@
+const test = require("node:test");
+const assert = require("node:assert");
+const { ConditionExpressionGroup, ConditionExpression } = require("../../../server/monitor-conditions/expression.js");
+
+test("Test ConditionExpressionGroup.fromMonitor", async (t) => {
+ const monitor = {
+ conditions: JSON.stringify([
+ {
+ "type": "expression",
+ "andOr": "and",
+ "operator": "contains",
+ "value": "foo",
+ "variable": "record"
+ },
+ {
+ "type": "group",
+ "andOr": "and",
+ "children": [
+ {
+ "type": "expression",
+ "andOr": "and",
+ "operator": "contains",
+ "value": "bar",
+ "variable": "record"
+ },
+ {
+ "type": "group",
+ "andOr": "and",
+ "children": [
+ {
+ "type": "expression",
+ "andOr": "and",
+ "operator": "contains",
+ "value": "car",
+ "variable": "record"
+ }
+ ]
+ },
+ ]
+ },
+ ]),
+ };
+ const root = ConditionExpressionGroup.fromMonitor(monitor);
+ assert.strictEqual(true, root.children.length === 2);
+ assert.strictEqual(true, root.children[0] instanceof ConditionExpression);
+ assert.strictEqual(true, root.children[0].value === "foo");
+ assert.strictEqual(true, root.children[1] instanceof ConditionExpressionGroup);
+ assert.strictEqual(true, root.children[1].children.length === 2);
+ assert.strictEqual(true, root.children[1].children[0] instanceof ConditionExpression);
+ assert.strictEqual(true, root.children[1].children[0].value === "bar");
+ assert.strictEqual(true, root.children[1].children[1] instanceof ConditionExpressionGroup);
+ assert.strictEqual(true, root.children[1].children[1].children.length === 1);
+ assert.strictEqual(true, root.children[1].children[1].children[0] instanceof ConditionExpression);
+ assert.strictEqual(true, root.children[1].children[1].children[0].value === "car");
+});
diff --git a/test/backend-test/monitor-conditions/test-operators.js b/test/backend-test/monitor-conditions/test-operators.js
new file mode 100644
index 0000000..e663c9a
--- /dev/null
+++ b/test/backend-test/monitor-conditions/test-operators.js
@@ -0,0 +1,108 @@
+const test = require("node:test");
+const assert = require("node:assert");
+const { operatorMap, OP_CONTAINS, OP_NOT_CONTAINS, OP_LT, OP_GT, OP_LTE, OP_GTE, OP_STR_EQUALS, OP_STR_NOT_EQUALS, OP_NUM_EQUALS, OP_NUM_NOT_EQUALS, OP_STARTS_WITH, OP_ENDS_WITH, OP_NOT_STARTS_WITH, OP_NOT_ENDS_WITH } = require("../../../server/monitor-conditions/operators.js");
+
+test("Test StringEqualsOperator", async (t) => {
+ const op = operatorMap.get(OP_STR_EQUALS);
+ assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.com"));
+ assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.org"));
+ assert.strictEqual(false, op.test("1", 1)); // strict equality
+});
+
+test("Test StringNotEqualsOperator", async (t) => {
+ const op = operatorMap.get(OP_STR_NOT_EQUALS);
+ assert.strictEqual(true, op.test("mx1.example.com", "mx1.example.org"));
+ assert.strictEqual(false, op.test("mx1.example.com", "mx1.example.com"));
+ assert.strictEqual(true, op.test(1, "1")); // variable is not typecasted (strict equality)
+});
+
+test("Test ContainsOperator with scalar", async (t) => {
+ const op = operatorMap.get(OP_CONTAINS);
+ assert.strictEqual(true, op.test("mx1.example.org", "example.org"));
+ assert.strictEqual(false, op.test("mx1.example.org", "example.com"));
+});
+
+test("Test ContainsOperator with array", async (t) => {
+ const op = operatorMap.get(OP_CONTAINS);
+ assert.strictEqual(true, op.test([ "example.org" ], "example.org"));
+ assert.strictEqual(false, op.test([ "example.org" ], "example.com"));
+});
+
+test("Test NotContainsOperator with scalar", async (t) => {
+ const op = operatorMap.get(OP_NOT_CONTAINS);
+ assert.strictEqual(true, op.test("example.org", ".com"));
+ assert.strictEqual(false, op.test("example.org", ".org"));
+});
+
+test("Test NotContainsOperator with array", async (t) => {
+ const op = operatorMap.get(OP_NOT_CONTAINS);
+ assert.strictEqual(true, op.test([ "example.org" ], "example.com"));
+ assert.strictEqual(false, op.test([ "example.org" ], "example.org"));
+});
+
+test("Test StartsWithOperator", async (t) => {
+ const op = operatorMap.get(OP_STARTS_WITH);
+ assert.strictEqual(true, op.test("mx1.example.com", "mx1"));
+ assert.strictEqual(false, op.test("mx1.example.com", "mx2"));
+});
+
+test("Test NotStartsWithOperator", async (t) => {
+ const op = operatorMap.get(OP_NOT_STARTS_WITH);
+ assert.strictEqual(true, op.test("mx1.example.com", "mx2"));
+ assert.strictEqual(false, op.test("mx1.example.com", "mx1"));
+});
+
+test("Test EndsWithOperator", async (t) => {
+ const op = operatorMap.get(OP_ENDS_WITH);
+ assert.strictEqual(true, op.test("mx1.example.com", "example.com"));
+ assert.strictEqual(false, op.test("mx1.example.com", "example.net"));
+});
+
+test("Test NotEndsWithOperator", async (t) => {
+ const op = operatorMap.get(OP_NOT_ENDS_WITH);
+ assert.strictEqual(true, op.test("mx1.example.com", "example.net"));
+ assert.strictEqual(false, op.test("mx1.example.com", "example.com"));
+});
+
+test("Test NumberEqualsOperator", async (t) => {
+ const op = operatorMap.get(OP_NUM_EQUALS);
+ assert.strictEqual(true, op.test(1, 1));
+ assert.strictEqual(true, op.test(1, "1"));
+ assert.strictEqual(false, op.test(1, "2"));
+});
+
+test("Test NumberNotEqualsOperator", async (t) => {
+ const op = operatorMap.get(OP_NUM_NOT_EQUALS);
+ assert.strictEqual(true, op.test(1, "2"));
+ assert.strictEqual(false, op.test(1, "1"));
+});
+
+test("Test LessThanOperator", async (t) => {
+ const op = operatorMap.get(OP_LT);
+ assert.strictEqual(true, op.test(1, 2));
+ assert.strictEqual(true, op.test(1, "2"));
+ assert.strictEqual(false, op.test(1, 1));
+});
+
+test("Test GreaterThanOperator", async (t) => {
+ const op = operatorMap.get(OP_GT);
+ assert.strictEqual(true, op.test(2, 1));
+ assert.strictEqual(true, op.test(2, "1"));
+ assert.strictEqual(false, op.test(1, 1));
+});
+
+test("Test LessThanOrEqualToOperator", async (t) => {
+ const op = operatorMap.get(OP_LTE);
+ assert.strictEqual(true, op.test(1, 1));
+ assert.strictEqual(true, op.test(1, 2));
+ assert.strictEqual(true, op.test(1, "2"));
+ assert.strictEqual(false, op.test(1, 0));
+});
+
+test("Test GreaterThanOrEqualToOperator", async (t) => {
+ const op = operatorMap.get(OP_GTE);
+ assert.strictEqual(true, op.test(1, 1));
+ assert.strictEqual(true, op.test(2, 1));
+ assert.strictEqual(true, op.test(2, "2"));
+ assert.strictEqual(false, op.test(2, 3));
+});
diff --git a/test/backend-test/test-mqtt.js b/test/backend-test/test-mqtt.js
new file mode 100644
index 0000000..4503102
--- /dev/null
+++ b/test/backend-test/test-mqtt.js
@@ -0,0 +1,102 @@
+const { describe, test } = require("node:test");
+const assert = require("node:assert");
+const { HiveMQContainer } = require("@testcontainers/hivemq");
+const mqtt = require("mqtt");
+const { MqttMonitorType } = require("../../server/monitor-types/mqtt");
+const { UP, PENDING } = require("../../src/util");
+
+/**
+ * Runs an MQTT test with the
+ * @param {string} mqttSuccessMessage the message that the monitor expects
+ * @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
+ * @param {string} receivedMessage what message is recieved from the mqtt channel
+ * @returns {Promise<Heartbeat>} the heartbeat produced by the check
+ */
+async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
+ const hiveMQContainer = await new HiveMQContainer().start();
+ const connectionString = hiveMQContainer.getConnectionString();
+ const mqttMonitorType = new MqttMonitorType();
+ const monitor = {
+ jsonPath: "firstProp", // always return firstProp for the json-query monitor
+ hostname: connectionString.split(":", 2).join(":"),
+ mqttTopic: "test",
+ port: connectionString.split(":")[2],
+ mqttUsername: null,
+ mqttPassword: null,
+ interval: 20, // controls the timeout
+ mqttSuccessMessage: mqttSuccessMessage, // for keywords
+ expectedValue: mqttSuccessMessage, // for json-query
+ mqttCheckType: mqttCheckType,
+ };
+ const heartbeat = {
+ msg: "",
+ status: PENDING,
+ };
+
+ const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
+ testMqttClient.on("connect", () => {
+ testMqttClient.subscribe("test", (error) => {
+ if (!error) {
+ testMqttClient.publish("test", receivedMessage);
+ }
+ });
+ });
+
+ try {
+ await mqttMonitorType.check(monitor, heartbeat, {});
+ } finally {
+ testMqttClient.end();
+ hiveMQContainer.stop();
+ }
+ return heartbeat;
+}
+
+describe("MqttMonitorType", {
+ concurrency: true,
+ skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
+}, () => {
+ test("valid keywords (type=default)", async () => {
+ const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
+ assert.strictEqual(heartbeat.status, UP);
+ assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
+ });
+
+ test("valid keywords (type=keyword)", async () => {
+ const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
+ assert.strictEqual(heartbeat.status, UP);
+ assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
+ });
+ test("invalid keywords (type=default)", async () => {
+ await assert.rejects(
+ testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
+ new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
+ );
+ });
+
+ test("invalid keyword (type=keyword)", async () => {
+ await assert.rejects(
+ testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
+ new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
+ );
+ });
+ test("valid json-query", async () => {
+ // works because the monitors' jsonPath is hard-coded to "firstProp"
+ const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
+ assert.strictEqual(heartbeat.status, UP);
+ assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
+ });
+ test("invalid (because query fails) json-query", async () => {
+ // 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("invalid (because successMessage fails) json-query", 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]")
+ );
+ });
+});
diff --git a/test/backend-test/test-rabbitmq.js b/test/backend-test/test-rabbitmq.js
new file mode 100644
index 0000000..5782ef2
--- /dev/null
+++ b/test/backend-test/test-rabbitmq.js
@@ -0,0 +1,53 @@
+const { describe, test } = require("node:test");
+const assert = require("node:assert");
+const { RabbitMQContainer } = require("@testcontainers/rabbitmq");
+const { RabbitMqMonitorType } = require("../../server/monitor-types/rabbitmq");
+const { UP, DOWN, PENDING } = require("../../src/util");
+
+describe("RabbitMQ Single Node", {
+ skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
+}, () => {
+ test("RabbitMQ is running", 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",
+ };
+
+ const heartbeat = {
+ msg: "",
+ status: PENDING,
+ };
+
+ try {
+ await rabbitMQMonitor.check(monitor, heartbeat, {});
+ assert.strictEqual(heartbeat.status, UP);
+ assert.strictEqual(heartbeat.msg, "OK");
+ } finally {
+ rabbitMQContainer.stop();
+ }
+ });
+
+ test("RabbitMQ is not running", async () => {
+ const rabbitMQMonitor = new RabbitMqMonitorType();
+ const monitor = {
+ rabbitmqNodes: JSON.stringify([ "http://localhost:15672" ]),
+ rabbitmqUsername: "rabbitmqUser",
+ rabbitmqPassword: "rabbitmqPass",
+ };
+
+ const heartbeat = {
+ msg: "",
+ status: PENDING,
+ };
+
+ await rabbitMQMonitor.check(monitor, heartbeat, {});
+ assert.strictEqual(heartbeat.status, DOWN);
+ });
+
+});
diff --git a/test/backend-test/test-uptime-calculator.js b/test/backend-test/test-uptime-calculator.js
new file mode 100644
index 0000000..4f2f05e
--- /dev/null
+++ b/test/backend-test/test-uptime-calculator.js
@@ -0,0 +1,425 @@
+const test = require("node:test");
+const assert = require("node:assert");
+const { UptimeCalculator } = require("../../server/uptime-calculator");
+const dayjs = require("dayjs");
+const { UP, DOWN, PENDING, MAINTENANCE } = require("../../src/util");
+dayjs.extend(require("dayjs/plugin/utc"));
+dayjs.extend(require("../../server/modules/dayjs/plugin/timezone"));
+dayjs.extend(require("dayjs/plugin/customParseFormat"));
+
+test("Test Uptime Calculator - custom date", async (t) => {
+ let c1 = new UptimeCalculator();
+
+ // Test custom date
+ UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z");
+ assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix());
+});
+
+test("Test update - UP", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
+ let c2 = new UptimeCalculator();
+ let date = await c2.update(UP);
+ assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix());
+});
+
+test("Test update - MAINTENANCE", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
+ let c2 = new UptimeCalculator();
+ let date = await c2.update(MAINTENANCE);
+ assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
+});
+
+test("Test update - DOWN", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
+ let c2 = new UptimeCalculator();
+ let date = await c2.update(DOWN);
+ assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
+});
+
+test("Test update - PENDING", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
+ let c2 = new UptimeCalculator();
+ let date = await c2.update(PENDING);
+ assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
+});
+
+test("Test flatStatus", async (t) => {
+ let c2 = new UptimeCalculator();
+ assert.strictEqual(c2.flatStatus(UP), UP);
+ //assert.strictEqual(c2.flatStatus(MAINTENANCE), UP);
+ assert.strictEqual(c2.flatStatus(DOWN), DOWN);
+ assert.strictEqual(c2.flatStatus(PENDING), DOWN);
+});
+
+test("Test getMinutelyKey", async (t) => {
+ let c2 = new UptimeCalculator();
+ let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00"));
+ assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
+
+ // Edge case 1
+ c2 = new UptimeCalculator();
+ divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01"));
+ assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
+
+ // Edge case 2
+ c2 = new UptimeCalculator();
+ divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59"));
+ assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
+});
+
+test("Test getDailyKey", async (t) => {
+ let c2 = new UptimeCalculator();
+ let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00"));
+ assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
+
+ c2 = new UptimeCalculator();
+ dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30"));
+ assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
+
+ // Edge case 1
+ c2 = new UptimeCalculator();
+ dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59"));
+ assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
+
+ // Edge case 2
+ c2 = new UptimeCalculator();
+ dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00"));
+ assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
+
+ // Test timezone
+ c2 = new UptimeCalculator();
+ dailyKey = c2.getDailyKey(dayjs("Sat Dec 23 2023 05:38:39 GMT+0800 (Hong Kong Standard Time)"));
+ assert.strictEqual(dailyKey, dayjs.utc("2023-12-22").unix());
+});
+
+test("Test lastDailyUptimeData", async (t) => {
+ let c2 = new UptimeCalculator();
+ await c2.update(UP);
+ assert.strictEqual(c2.lastDailyUptimeData.up, 1);
+});
+
+test("Test get24Hour Uptime and Avg Ping", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
+
+ // No data
+ let c2 = new UptimeCalculator();
+ let data = c2.get24Hour();
+ assert.strictEqual(data.uptime, 0);
+ assert.strictEqual(data.avgPing, null);
+
+ // 1 Up
+ c2 = new UptimeCalculator();
+ await c2.update(UP, 100);
+ let uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 1);
+ assert.strictEqual(c2.get24Hour().avgPing, 100);
+
+ // 2 Up
+ c2 = new UptimeCalculator();
+ await c2.update(UP, 100);
+ await c2.update(UP, 200);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 1);
+ assert.strictEqual(c2.get24Hour().avgPing, 150);
+
+ // 3 Up
+ c2 = new UptimeCalculator();
+ await c2.update(UP, 0);
+ await c2.update(UP, 100);
+ await c2.update(UP, 400);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 1);
+ assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666);
+
+ // 1 MAINTENANCE
+ c2 = new UptimeCalculator();
+ await c2.update(MAINTENANCE);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0);
+ assert.strictEqual(c2.get24Hour().avgPing, null);
+
+ // 1 PENDING
+ c2 = new UptimeCalculator();
+ await c2.update(PENDING);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0);
+ assert.strictEqual(c2.get24Hour().avgPing, null);
+
+ // 1 DOWN
+ c2 = new UptimeCalculator();
+ await c2.update(DOWN);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0);
+ assert.strictEqual(c2.get24Hour().avgPing, null);
+
+ // 2 DOWN
+ c2 = new UptimeCalculator();
+ await c2.update(DOWN);
+ await c2.update(DOWN);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0);
+ assert.strictEqual(c2.get24Hour().avgPing, null);
+
+ // 1 DOWN, 1 UP
+ c2 = new UptimeCalculator();
+ await c2.update(DOWN);
+ await c2.update(UP, 0.5);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0.5);
+ assert.strictEqual(c2.get24Hour().avgPing, 0.5);
+
+ // 1 UP, 1 DOWN
+ c2 = new UptimeCalculator();
+ await c2.update(UP, 123);
+ await c2.update(DOWN);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0.5);
+ assert.strictEqual(c2.get24Hour().avgPing, 123);
+
+ // Add 24 hours
+ c2 = new UptimeCalculator();
+ await c2.update(UP, 0);
+ await c2.update(UP, 0);
+ await c2.update(UP, 0);
+ await c2.update(UP, 1);
+ await c2.update(DOWN);
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0.8);
+ assert.strictEqual(c2.get24Hour().avgPing, 0.25);
+
+ UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
+
+ // After 24 hours, even if there is no data, the uptime should be still 80%
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0.8);
+ assert.strictEqual(c2.get24Hour().avgPing, 0.25);
+
+ // Add more 24 hours (48 hours)
+ UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
+
+ // After 48 hours, even if there is no data, the uptime should be still 80%
+ uptime = c2.get24Hour().uptime;
+ assert.strictEqual(uptime, 0.8);
+ assert.strictEqual(c2.get24Hour().avgPing, 0.25);
+});
+
+test("Test get7DayUptime", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
+
+ // No data
+ let c2 = new UptimeCalculator();
+ let uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0);
+
+ // 1 Up
+ c2 = new UptimeCalculator();
+ await c2.update(UP);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 1);
+
+ // 2 Up
+ c2 = new UptimeCalculator();
+ await c2.update(UP);
+ await c2.update(UP);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 1);
+
+ // 3 Up
+ c2 = new UptimeCalculator();
+ await c2.update(UP);
+ await c2.update(UP);
+ await c2.update(UP);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 1);
+
+ // 1 MAINTENANCE
+ c2 = new UptimeCalculator();
+ await c2.update(MAINTENANCE);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0);
+
+ // 1 PENDING
+ c2 = new UptimeCalculator();
+ await c2.update(PENDING);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0);
+
+ // 1 DOWN
+ c2 = new UptimeCalculator();
+ await c2.update(DOWN);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0);
+
+ // 2 DOWN
+ c2 = new UptimeCalculator();
+ await c2.update(DOWN);
+ await c2.update(DOWN);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0);
+
+ // 1 DOWN, 1 UP
+ c2 = new UptimeCalculator();
+ await c2.update(DOWN);
+ await c2.update(UP);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0.5);
+
+ // 1 UP, 1 DOWN
+ c2 = new UptimeCalculator();
+ await c2.update(UP);
+ await c2.update(DOWN);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0.5);
+
+ // Add 7 days
+ c2 = new UptimeCalculator();
+ await c2.update(UP);
+ await c2.update(UP);
+ await c2.update(UP);
+ await c2.update(UP);
+ await c2.update(DOWN);
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0.8);
+ UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day");
+
+ // After 7 days, even if there is no data, the uptime should be still 80%
+ uptime = c2.get7Day().uptime;
+ assert.strictEqual(uptime, 0.8);
+
+});
+
+test("Test get30DayUptime (1 check per day)", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
+
+ let c2 = new UptimeCalculator();
+ let uptime = c2.get30Day().uptime;
+ assert.strictEqual(uptime, 0);
+
+ let up = 0;
+ let down = 0;
+ let flip = true;
+ for (let i = 0; i < 30; i++) {
+ UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
+
+ if (flip) {
+ await c2.update(UP);
+ up++;
+ } else {
+ await c2.update(DOWN);
+ down++;
+ }
+
+ uptime = c2.get30Day().uptime;
+ assert.strictEqual(uptime, up / (up + down));
+
+ flip = !flip;
+ }
+
+ // Last 7 days
+ // Down, Up, Down, Up, Down, Up, Down
+ // So 3 UP
+ assert.strictEqual(c2.get7Day().uptime, 3 / 7);
+});
+
+test("Test get1YearUptime (1 check per day)", async (t) => {
+ UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
+
+ let c2 = new UptimeCalculator();
+ let uptime = c2.get1Year().uptime;
+ assert.strictEqual(uptime, 0);
+
+ let flip = true;
+ for (let i = 0; i < 365; i++) {
+ UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
+
+ if (flip) {
+ await c2.update(UP);
+ } else {
+ await c2.update(DOWN);
+ }
+
+ uptime = c2.get30Day().time;
+ flip = !flip;
+ }
+
+ assert.strictEqual(c2.get1Year().uptime, 183 / 365);
+ assert.strictEqual(c2.get30Day().uptime, 15 / 30);
+ assert.strictEqual(c2.get7Day().uptime, 4 / 7);
+});
+
+/**
+ * Code from here: https://stackoverflow.com/a/64550489/1097815
+ * @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 memoryData = process.memoryUsage();
+
+ return {
+ rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`,
+ heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`,
+ heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`,
+ 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));
+ });
+
+});
diff --git a/test/e2e/specs/example.spec.js b/test/e2e/specs/example.spec.js
new file mode 100644
index 0000000..4fcfac6
--- /dev/null
+++ b/test/e2e/specs/example.spec.js
@@ -0,0 +1,39 @@
+import { expect, test } from "@playwright/test";
+import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
+
+test.describe("Example Spec", () => {
+
+ test.beforeEach(async ({ page }) => {
+ await restoreSqliteSnapshot(page);
+ });
+
+ test("dashboard", async ({ page }, testInfo) => {
+ await page.goto("./dashboard");
+ await login(page);
+ await screenshot(testInfo, page);
+ });
+
+ test("set up monitor", async ({ page }, testInfo) => {
+ await page.goto("./add");
+ await login(page);
+
+ await expect(page.getByTestId("monitor-type-select")).toBeVisible();
+ await page.getByTestId("monitor-type-select").selectOption("http");
+ await page.getByTestId("friendly-name-input").fill("example.com");
+ await page.getByTestId("url-input").fill("https://www.example.com/");
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
+
+ await expect(page.getByTestId("monitor-list")).toContainText("example.com");
+ await screenshot(testInfo, page);
+ });
+
+ test("database is reset after previous test", async ({ page }, testInfo) => {
+ await page.goto("./dashboard");
+ await login(page);
+
+ await expect(page.getByTestId("monitor-list")).not.toContainText("example.com");
+ await screenshot(testInfo, page);
+ });
+
+});
diff --git a/test/e2e/specs/monitor-form.spec.js b/test/e2e/specs/monitor-form.spec.js
new file mode 100644
index 0000000..7a84f3c
--- /dev/null
+++ b/test/e2e/specs/monitor-form.spec.js
@@ -0,0 +1,104 @@
+import { expect, test } from "@playwright/test";
+import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
+
+/**
+ * Selects the monitor type from the dropdown.
+ * @param {import('@playwright/test').Page} page - The Playwright page instance.
+ * @param {string} monitorType - The monitor type to select (default is "dns").
+ * @returns {Promise<void>} - A promise that resolves when the monitor type is selected.
+ */
+async function selectMonitorType(page, monitorType = "dns") {
+ const monitorTypeSelect = page.getByTestId("monitor-type-select");
+ await expect(monitorTypeSelect).toBeVisible();
+ await monitorTypeSelect.selectOption(monitorType);
+
+ const selectedValue = await monitorTypeSelect.evaluate((select) => select.value);
+ expect(selectedValue).toBe(monitorType);
+}
+
+test.describe("Monitor Form", () => {
+ test.beforeEach(async ({ page }) => {
+ await restoreSqliteSnapshot(page);
+ });
+
+ test("condition ui", async ({ page }, testInfo) => {
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+ await selectMonitorType(page);
+
+ await page.getByTestId("add-condition-button").click();
+ expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
+
+ await page.getByTestId("add-group-button").click();
+ expect(await page.getByTestId("condition-group").count()).toEqual(1);
+ expect(await page.getByTestId("condition").count()).toEqual(3); // 2 solo conditions + 1 condition in group
+
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("remove-condition").first().click();
+ expect(await page.getByTestId("condition").count()).toEqual(2); // 1 solo condition + 1 condition in group
+
+ await page.getByTestId("remove-condition-group").first().click();
+ expect(await page.getByTestId("condition-group").count()).toEqual(0);
+
+ await screenshot(testInfo, page);
+ });
+
+ test("successful condition", async ({ page }, testInfo) => {
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+ await selectMonitorType(page);
+
+ const friendlyName = "Example DNS NS";
+ await page.getByTestId("friendly-name-input").fill(friendlyName);
+ await page.getByTestId("hostname-input").fill("example.com");
+
+ const resolveTypeSelect = page.getByTestId("resolve-type-select");
+ await resolveTypeSelect.click();
+ await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
+
+ await page.getByTestId("add-condition-button").click();
+ expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added
+
+ await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net");
+ await page.getByTestId("condition-and-or").nth(0).selectOption("or");
+ await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net");
+
+ await screenshot(testInfo, page);
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*");
+
+ expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
+
+ await screenshot(testInfo, page);
+ });
+
+ test("failing condition", async ({ page }, testInfo) => {
+ await page.goto("./add");
+ await login(page);
+ await screenshot(testInfo, page);
+ await selectMonitorType(page);
+
+ const friendlyName = "Example DNS NS";
+ await page.getByTestId("friendly-name-input").fill(friendlyName);
+ await page.getByTestId("hostname-input").fill("example.com");
+
+ const resolveTypeSelect = page.getByTestId("resolve-type-select");
+ await resolveTypeSelect.click();
+ await resolveTypeSelect.getByRole("option", { name: "NS" }).click();
+
+ expect(await page.getByTestId("condition").count()).toEqual(1); // 1 added by default
+
+ await page.getByTestId("condition-value").nth(0).fill("definitely-not.net");
+
+ await screenshot(testInfo, page);
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*");
+
+ expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true });
+
+ await screenshot(testInfo, page);
+ });
+});
diff --git a/test/e2e/specs/setup-process.once.js b/test/e2e/specs/setup-process.once.js
new file mode 100644
index 0000000..f1bdf2b
--- /dev/null
+++ b/test/e2e/specs/setup-process.once.js
@@ -0,0 +1,55 @@
+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) => {
+ await screenshot(testInfo, page);
+ });
+
+ /*
+ * Setup
+ */
+
+ test("setup sqlite", async ({ page }, testInfo) => {
+ await page.goto("./");
+ await page.getByText("SQLite").click();
+ await page.getByRole("button", { name: "Next" }).click();
+ await screenshot(testInfo, page);
+ await page.waitForURL("/setup"); // ensures the server is ready to continue to the next test
+ });
+
+ test("setup admin", async ({ page }) => {
+ await page.goto("./");
+ await page.getByPlaceholder("Username").click();
+ await page.getByPlaceholder("Username").fill("admin");
+ await page.getByPlaceholder("Username").press("Tab");
+ await page.getByPlaceholder("Password", { exact: true }).fill("admin123");
+ await page.getByPlaceholder("Password", { exact: true }).press("Tab");
+ await page.getByPlaceholder("Repeat Password").fill("admin123");
+ await page.getByRole("button", { name: "Create" }).click();
+ });
+
+ /*
+ * All other tests should be run after setup
+ */
+
+ test("login", async ({ page }) => {
+ await page.goto("./dashboard");
+ await login(page);
+ });
+
+ test("logout", async ({ page }) => {
+ await page.goto("./dashboard");
+ await login(page);
+ await page.getByText("A", { exact: true }).click();
+ await page.getByRole("button", { name: "Log out" }).click();
+ });
+
+ test("take sqlite snapshot", async ({ page }) => {
+ await takeSqliteSnapshot(page);
+ });
+
+});
diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js
new file mode 100644
index 0000000..f525dfc
--- /dev/null
+++ b/test/e2e/specs/status-page.spec.js
@@ -0,0 +1,129 @@
+import { expect, test } from "@playwright/test";
+import { login, restoreSqliteSnapshot, screenshot } from "../util-test";
+
+test.describe("Status Page", () => {
+
+ test.beforeEach(async ({ page }) => {
+ await restoreSqliteSnapshot(page);
+ });
+
+ test("create and edit", async ({ page }, testInfo) => {
+ // Monitor
+ const monitorName = "Monitor for Status Page";
+ const tagName = "Client";
+ const tagValue = "Acme Inc";
+
+ // Status Page
+ const footerText = "This is footer text.";
+ const refreshInterval = 30;
+ const theme = "dark";
+ const googleAnalyticsId = "G-123";
+ const customCss = "body { background: rgb(0, 128, 128) !important; }";
+ const descriptionText = "This is an example status page.";
+ const incidentTitle = "Example Outage Incident";
+ const incidentContent = "Sample incident message.";
+ const groupName = "Example Group 1";
+
+ // Set up a monitor that can be added to the Status Page
+ await page.goto("./add");
+ await login(page);
+ await expect(page.getByTestId("monitor-type-select")).toBeVisible();
+ await page.getByTestId("monitor-type-select").selectOption("http");
+ await page.getByTestId("friendly-name-input").fill(monitorName);
+ await page.getByTestId("url-input").fill("https://www.example.com/");
+ await page.getByTestId("add-tag-button").click();
+ await page.getByTestId("tag-name-input").fill(tagName);
+ await page.getByTestId("tag-value-input").fill(tagValue);
+ await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component
+ await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click();
+ await page.getByTestId("tag-submit-button").click();
+ await page.getByTestId("save-button").click();
+ await page.waitForURL("/dashboard/*"); // wait for the monitor to be created
+
+ // Create a new status page
+ await page.goto("./add-status-page");
+ await screenshot(testInfo, page);
+
+ await page.getByTestId("name-input").fill("Example");
+ await page.getByTestId("slug-input").fill("example");
+ await page.getByTestId("submit-button").click();
+ await page.waitForURL("/status/example?edit"); // wait for the page to be created
+
+ // Fill in some details
+ await page.getByTestId("description-input").fill(descriptionText);
+ await page.getByTestId("footer-text-input").fill(footerText);
+ await page.getByTestId("refresh-interval-input").fill(String(refreshInterval));
+ await page.getByTestId("theme-select").selectOption(theme);
+ await page.getByTestId("show-tags-checkbox").uncheck();
+ await page.getByTestId("show-powered-by-checkbox").uncheck();
+ await page.getByTestId("show-certificate-expiry-checkbox").uncheck();
+ await page.getByTestId("google-analytics-input").fill(googleAnalyticsId);
+ await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism
+ await expect(page.getByTestId("description-editable")).toHaveText(descriptionText);
+ await expect(page.getByTestId("custom-footer-editable")).toHaveText(footerText);
+
+ // Add an incident
+ await page.getByTestId("create-incident-button").click();
+ await page.getByTestId("incident-title").isEditable();
+ await page.getByTestId("incident-title").fill(incidentTitle);
+ await page.getByTestId("incident-content-editable").fill(incidentContent);
+ await page.getByTestId("post-incident-button").click();
+
+ // Add a group
+ await page.getByTestId("add-group-button").click();
+ await page.getByTestId("group-name").isEditable();
+ await page.getByTestId("group-name").fill(groupName);
+
+ // Add the monitor
+ await page.getByTestId("monitor-select").click(); // Vue-Multiselect component
+ await page.getByTestId("monitor-select").getByRole("option", { name: monitorName }).click();
+ await expect(page.getByTestId("monitor")).toHaveCount(1);
+ await expect(page.getByTestId("monitor-name")).toContainText(monitorName);
+
+ // Save the changes
+ await screenshot(testInfo, page);
+ await page.getByTestId("save-button").click();
+ await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
+
+ // Ensure changes are visible
+ await expect(page.getByTestId("incident")).toHaveCount(1);
+ await expect(page.getByTestId("incident-title")).toContainText(incidentTitle);
+ await expect(page.getByTestId("incident-content")).toContainText(incidentContent);
+ await expect(page.getByTestId("description")).toContainText(descriptionText);
+ await expect(page.getByTestId("group-name")).toContainText(groupName);
+ await expect(page.getByTestId("footer-text")).toContainText(footerText);
+ await expect(page.getByTestId("powered-by")).toHaveCount(0);
+
+ await expect(page.getByTestId("update-countdown-text")).toContainText("00:");
+ const updateCountdown = Number((await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]);
+ expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval); // cant be certain when the timer will start, so ensure it's within expected range
+ expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10);
+
+ await expect(page.locator("body")).toHaveClass(theme);
+ expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId);
+
+ const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor);
+ expect(backgroundColor).toEqual("rgb(0, 128, 128)");
+
+ await screenshot(testInfo, page);
+
+ // Flip the "Show Tags" and "Show Powered By" switches:
+ await page.getByTestId("edit-button").click();
+ await expect(page.getByTestId("edit-sidebar")).toHaveCount(1);
+ await page.getByTestId("show-tags-checkbox").setChecked(true);
+ await page.getByTestId("show-powered-by-checkbox").setChecked(true);
+
+ await screenshot(testInfo, page);
+ await page.getByTestId("save-button").click();
+
+ await expect(page.getByTestId("edit-sidebar")).toHaveCount(0);
+ await expect(page.getByTestId("powered-by")).toContainText("Powered by");
+ await expect(page.getByTestId("monitor-tag")).toContainText(tagValue);
+
+ await screenshot(testInfo, page);
+ });
+
+ // @todo Test certificate expiry
+ // @todo Test domain names
+
+});
diff --git a/test/e2e/util-test.js b/test/e2e/util-test.js
new file mode 100644
index 0000000..f6af3cb
--- /dev/null
+++ b/test/e2e/util-test.js
@@ -0,0 +1,62 @@
+const fs = require("fs");
+const path = require("path");
+const serverUrl = require("../../config/playwright.config.js").url;
+
+const dbPath = "./../../data/playwright-test/kuma.db";
+
+/**
+ * @param {TestInfo} testInfo Test info
+ * @param {Page} page Page
+ * @returns {Promise<void>}
+ */
+export async function screenshot(testInfo, page) {
+ const screenshot = await page.screenshot();
+ await testInfo.attach("screenshot", {
+ body: screenshot,
+ contentType: "image/png"
+ });
+}
+
+/**
+ * @param {Page} page Page
+ * @returns {Promise<void>}
+ */
+export async function login(page) {
+ // Login
+ await page.getByPlaceholder("Username").click();
+ await page.getByPlaceholder("Username").fill("admin");
+ await page.getByPlaceholder("Username").press("Tab");
+ await page.getByPlaceholder("Password").fill("admin123");
+ await page.getByLabel("Remember me").check();
+ await page.getByRole("button", { name: "Log in" }).click();
+ await page.isVisible("text=Add New Monitor");
+}
+
+/**
+ * Determines if the SQLite database has been created. This indicates setup has completed.
+ * @returns {boolean} True if exists
+ */
+export function getSqliteDatabaseExists() {
+ return fs.existsSync(path.resolve(__dirname, dbPath));
+}
+
+/**
+ * Makes a request to the server to take a snapshot of the SQLite database.
+ * @param {Page|null} page Page
+ * @returns {Promise<Response>} Promise of response from snapshot request.
+ */
+export async function takeSqliteSnapshot(page = null) {
+ if (page) {
+ return page.goto("./_e2e/take-sqlite-snapshot");
+ } else {
+ return fetch(`${serverUrl}/_e2e/take-sqlite-snapshot`);
+ }
+}
+
+/**
+ * Makes a request to the server to restore the snapshot of the SQLite database.
+ * @returns {Promise<Response>} Promise of response from restoration request.
+ */
+export async function restoreSqliteSnapshot() {
+ return fetch(`${serverUrl}/_e2e/restore-sqlite-snapshot`);
+}
diff --git a/test/prepare-test-server.js b/test/prepare-test-server.js
new file mode 100644
index 0000000..1e0ed5c
--- /dev/null
+++ b/test/prepare-test-server.js
@@ -0,0 +1,10 @@
+const fs = require("fs");
+
+const path = "./data/test";
+
+if (fs.existsSync(path)) {
+ fs.rmSync(path, {
+ recursive: true,
+ force: true,
+ });
+}
diff --git a/test/test-radius.dockerfile b/test/test-radius.dockerfile
new file mode 100644
index 0000000..3f577ed
--- /dev/null
+++ b/test/test-radius.dockerfile
@@ -0,0 +1,13 @@
+# Container running a test radius server
+# More instructions in https://github.com/louislam/uptime-kuma/pull/1635
+
+FROM freeradius/freeradius-server:latest
+
+RUN mkdir -p /etc/raddb/mods-config/files/
+
+RUN echo "client net {" > /etc/raddb/clients.conf
+RUN echo " ipaddr = 172.17.0.0/16" >> /etc/raddb/clients.conf
+RUN echo " secret = testing123" >> /etc/raddb/clients.conf
+RUN echo "}" >> /etc/raddb/clients.conf
+
+RUN echo "bob Cleartext-Password := \"testpw\"" > /etc/raddb/mods-config/files/authorize