// biome-ignore assist/source/organizeImports: import mocks first import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; import {mockLogger} from "./mocks/logger"; import {initSdNotify} from "../lib/util/sd-notify"; const mockPlatform = vi.fn(() => "linux"); vi.mock("node:os", () => ({ platform: vi.fn(() => mockPlatform()), })); const mockUnixDgramSocket = { send: vi.fn(), }; const mockCreateSocket = vi.fn(() => { if (mockPlatform() !== "win32") { return mockUnixDgramSocket; } throw new Error("Unix datagrams not available on this platform"); }); vi.mock("unix-dgram", () => ({ createSocket: mockCreateSocket, })); const mocksClear = [ mockLogger.log, mockLogger.debug, mockLogger.info, mockLogger.warning, mockLogger.error, mockUnixDgramSocket.send, mockCreateSocket, mockPlatform, ]; describe("sd-notify", () => { const expectSocketNthSend = (nth: number, message: string): void => { expect(mockUnixDgramSocket.send).toHaveBeenNthCalledWith(nth, Buffer.from(message), 0, expect.any(Number), "mocked", expect.any(Function)); }; beforeAll(() => { vi.useFakeTimers(); }); afterAll(() => { vi.useRealTimers(); }); beforeEach(() => { for (const mock of mocksClear) mock.mockClear(); delete process.env.NOTIFY_SOCKET; delete process.env.WATCHDOG_USEC; delete process.env.WSL_DISTRO_NAME; }); it("No socket", async () => { const res = await initSdNotify(); expect(mockCreateSocket).toHaveBeenCalledTimes(0); expect(res).toBeUndefined(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(0); }); it("Error on unsupported platform", async () => { // also called by `mockCreateSocket` mockPlatform.mockImplementationOnce(() => "win32").mockImplementationOnce(() => "win32"); process.env.NOTIFY_SOCKET = "mocked"; const res = await initSdNotify(); expect(mockCreateSocket).toHaveBeenCalledTimes(1); expect(res).toBeUndefined(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(0); expect(mockLogger.warning).toHaveBeenCalledWith("NOTIFY_SOCKET env is set: Unix datagrams not available on this platform"); }); it("Error on supported platform", async () => { // NOTE: `import('unix-dgram')` can also fail in similar way when bindings are missing (not compiled) mockCreateSocket.mockImplementationOnce(() => { throw new Error("Error create socket"); }); process.env.NOTIFY_SOCKET = "mocked"; const res = await initSdNotify(); expect(mockCreateSocket).toHaveBeenCalledTimes(1); expect(res).toBeUndefined(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(0); expect(mockLogger.error).toHaveBeenCalledWith("Could not init sd_notify: Error create socket"); }); it("Socket only", async () => { process.env.NOTIFY_SOCKET = "mocked"; const res = await initSdNotify(); expect(res).toStrictEqual({notifyStopping: expect.any(Function), stop: expect.any(Function)}); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(1); expectSocketNthSend(1, "READY=1"); await vi.advanceTimersByTimeAsync(7500); res!.notifyStopping(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(2); expectSocketNthSend(2, "STOPPING=1"); res!.stop(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(2); }); it("Invalid watchdog timeout - socket only", async () => { process.env.NOTIFY_SOCKET = "mocked"; process.env.WATCHDOG_USEC = "mocked"; const res = await initSdNotify(); expect(res).toStrictEqual({notifyStopping: expect.any(Function), stop: expect.any(Function)}); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(1); expectSocketNthSend(1, "READY=1"); await vi.advanceTimersByTimeAsync(7500); res!.notifyStopping(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(2); expectSocketNthSend(2, "STOPPING=1"); res!.stop(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(2); }); it("Socket and watchdog", async () => { process.env.NOTIFY_SOCKET = "mocked"; process.env.WATCHDOG_USEC = "10000000"; const res = await initSdNotify(); expect(res).toStrictEqual({notifyStopping: expect.any(Function), stop: expect.any(Function)}); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(1); expectSocketNthSend(1, "READY=1"); await vi.advanceTimersByTimeAsync(7500); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(2); expectSocketNthSend(2, "WATCHDOG=1"); res!.notifyStopping(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(3); expectSocketNthSend(3, "STOPPING=1"); await vi.advanceTimersByTimeAsync(6000); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(4); expectSocketNthSend(4, "WATCHDOG=1"); res!.stop(); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(4); await vi.advanceTimersByTimeAsync(10000); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(4); }); it("Fails to send", async () => { mockUnixDgramSocket.send.mockImplementationOnce( (_buf: Buffer, _offset: number, _length: number, _path: string, callback?: (err?: Error) => void) => { callback!(new Error("Failure")); }, ); process.env.NOTIFY_SOCKET = "mocked"; const res = await initSdNotify(); expect(res).toStrictEqual({notifyStopping: expect.any(Function), stop: expect.any(Function)}); expect(mockUnixDgramSocket.send).toHaveBeenCalledTimes(1); expectSocketNthSend(1, "READY=1"); expect(mockLogger.warning).toHaveBeenCalledWith(`Failed to send "READY=1" to systemd: Failure`); }); });