diff --git a/test/backend-test/test-system-service.js b/test/backend-test/test-system-service.js index bd44cdefc..e4cbc6066 100644 --- a/test/backend-test/test-system-service.js +++ b/test/backend-test/test-system-service.js @@ -1,12 +1,18 @@ -const { describe, it, beforeEach } = require("node:test"); +const { describe, it, beforeEach, afterEach } = require("node:test"); const assert = require("node:assert"); const { SystemServiceMonitorType } = require("../../server/monitor-types/system-service"); const { DOWN, UP } = require("../../src/util"); const process = require("process"); +const fs = require("fs"); +const path = require("path"); +const os = require("os"); describe("SystemServiceMonitorType", () => { let monitorType; let heartbeat; + let tempDir; + let originalPath; + let originalPlatform; beforeEach(() => { monitorType = new SystemServiceMonitorType(); @@ -14,85 +20,107 @@ describe("SystemServiceMonitorType", () => { status: DOWN, msg: "", }; + originalPath = process.env.PATH; + originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); }); - it("should detect a running service", async (t) => { - if (process.platform !== "linux" && process.platform !== "win32") { - t.skip("Skipping integration test on unsupported platform"); - return; + afterEach(() => { + process.env.PATH = originalPath; + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform); } - - // Use core services that are guaranteed to be running - let serviceName = "dummy"; - if (process.platform === "linux") { - serviceName = "systemd-journald"; - } else if (process.platform === "win32") { - serviceName = "Dnscache"; - } - - const monitor = { system_service_name: serviceName }; - - try { - await monitorType.check(monitor, heartbeat); - } catch (e) { - // GitHub Actions runners often run inside containers without systemd (PID 1). - // In this specific case, we skip the test instead of failing. - const msg = e.message || ""; - if (msg.includes("System has not been booted with systemd") || - msg.includes("Host is down") || - msg.includes("Connect to D-Bus")) { - t.skip("Skipping: CI environment does not support systemd/D-Bus"); - return; + if (tempDir && fs.existsSync(tempDir)) { + try { + fs.rmSync(tempDir, { + recursive: true, + force: true, + }); + } catch (e) { + // Ignore cleanup errors } - // If it failed for any other reason, rethrow (fail the test) - throw e; } + }); + + /** + * Helper to create a fake executable that prints specific output. + * @param {string} baseName The name of the executable (e.g. systemctl) + * @param {string} outputText The text to echo to stdout + * @param {number} exitCode The exit code + * @returns {void} + */ + function createMockCommand(baseName, outputText, exitCode = 0) { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "uptime-kuma-test-")); + const fileName = process.platform === "win32" ? baseName + ".cmd" : baseName; + + let content; + if (process.platform === "win32") { + content = `@echo off\necho ${outputText}\nexit /b ${exitCode}`; + } else { + content = `#!/bin/sh\necho "${outputText}"\nexit ${exitCode}`; + } + + const scriptPath = path.join(tempDir, fileName); + fs.writeFileSync(scriptPath, content); + fs.chmodSync(scriptPath, 0o755); + process.env.PATH = tempDir + path.delimiter + process.env.PATH; + } + + it("should detect a running service", async () => { + const isWin = process.platform === "win32"; + // We mock the command to output "active", but the Monitor implementation + // abstracts this away into a friendly message like "Service ... is running." + createMockCommand(isWin ? "powershell" : "systemctl", isWin ? "Running" : "active", 0); + + if (process.platform === "darwin") { + Object.defineProperty(process, "platform", { + value: "linux", + configurable: true, + }); + } + + const monitor = { + system_service_name: "myservice", + }; + + await monitorType.check(monitor, heartbeat); assert.strictEqual(heartbeat.status, UP); - assert.ok(heartbeat.msg && heartbeat.msg.length > 0); + assert.ok(heartbeat.msg.includes("is running")); }); - it("should handle non-existent service gracefully", async (t) => { - // Skip this test on macOS/Docker runners where systemctl doesn't exist - if (process.platform !== "linux" && process.platform !== "win32") { - t.skip("Skipping integration test on unsupported platform"); - return; + it("should detect a stopped service", async () => { + const isWin = process.platform === "win32"; + createMockCommand(isWin ? "powershell" : "systemctl", isWin ? "Stopped" : "inactive", 1); + + if (process.platform === "darwin") { + Object.defineProperty(process, "platform", { + value: "linux", + configurable: true, + }); } - const monitor = { system_service_name: "non-existent-service-12345" }; + const monitor = { + system_service_name: "myservice", + }; try { await monitorType.check(monitor, heartbeat); } catch (e) { - // GitHub Actions workaround: If systemd is missing, we can't test "non-existent" - // because the tool itself fails before checking the service. - const msg = e.message || ""; - if (msg.includes("System has not been booted with systemd") || msg.includes("Host is down")) { - t.skip("Skipping: CI environment does not support systemd"); - return; - } - // Otherwise, we expect a failure for non-existent service, so we catch it. - } - - // If we skipped above, this assertion won't be reached. - // If we didn't skip, heartbeat should be DOWN. - if (heartbeat.msg && heartbeat.msg.includes("System has not been booted with systemd")) { - // Redundant check, but ensures safety - return; + // Expected error } assert.strictEqual(heartbeat.status, DOWN); - // Ensure some output was captured from the command - assert.ok(heartbeat.msg && heartbeat.msg.length > 0); }); it("should fail gracefully with invalid characters", async () => { Object.defineProperty(process, "platform", { value: "linux", - configurable: true + configurable: true, }); - const monitor = { system_service_name: "invalid&service;name" }; + const monitor = { + system_service_name: "invalid&service;name", + }; try { await monitorType.check(monitor, heartbeat); @@ -104,20 +132,15 @@ describe("SystemServiceMonitorType", () => { }); it("should throw error on unsupported platforms", async () => { - // Mock the platform to be 'darwin' (macOS) Object.defineProperty(process, "platform", { value: "darwin", - configurable: true + configurable: true, }); - const monitor = { system_service_name: "test-service" }; + const monitor = { + system_service_name: "test-service", + }; - await assert.rejects( - async () => await monitorType.check(monitor, heartbeat), - (err) => { - assert.match(err.message, /not supported on/); - return true; - } - ); + await assert.rejects(async () => await monitorType.check(monitor, heartbeat), /not supported/); }); });