IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

power-service.test.ts

Open raw
import type * as NodeFsPromises from "node:fs/promises";
import type * as DataRootModule from "@/lib/server/storage/data-root";
import { beforeEach, describe, expect, it, vi } from "vitest";

const { execFileMock, statMock } = vi.hoisted(() => ({
  execFileMock: vi.fn(),
  statMock: vi.fn(),
}));

const { serverEnvMock } = vi.hoisted(() => ({
  serverEnvMock: {
    DATABASE_URL: "postgres://postgres:postgres@localhost:5432/homeio",
  },
}));

vi.mock("node:child_process", () => ({
  execFile: execFileMock,
}));

vi.mock("node:fs/promises", async (importOriginal) => {
  const original = await importOriginal<typeof NodeFsPromises>();
  return { ...original, stat: statMock };
});

vi.mock("@/lib/server/env", () => ({
  serverEnv: serverEnvMock,
}));

vi.mock("@/lib/server/storage/data-root", async (importOriginal) => {
  const actual =
    await importOriginal<typeof DataRootModule>();
  return {
    ...actual,
    resolveDataRootDirectory: () => "/DATA",
    resolveStoreStacksRoot: () => "/DATA/Apps",
  };
});

import {
  scheduleFactoryReset,
  scheduleSystemReboot,
  scheduleSystemShutdown,
} from "@/lib/server/modules/system/power-service";

function resolveExecFileCallback(args: unknown[]) {
  const maybeCallback = args.at(-1);
  if (typeof maybeCallback !== "function") {
    throw new Error("Expected execFile callback");
  }

  return maybeCallback as (
    error: Error | null,
    stdout?: string,
    stderr?: string,
  ) => void;
}

describe("power-service", () => {
  beforeEach(() => {
    execFileMock.mockReset();
    statMock.mockReset();
    // Script exists by default
    statMock.mockResolvedValue({});
    execFileMock.mockImplementation(
      (_command: string, _args: string[], ...rest: unknown[]) => {
        const callback = resolveExecFileCallback(rest);
        callback(null, "", "");
      },
    );
  });

  it("schedules reboot through a transient systemd unit", async () => {
    await scheduleSystemReboot();

    expect(execFileMock).toHaveBeenCalledWith(
      "systemd-run",
      expect.arrayContaining([
        "--quiet",
        "--no-block",
        "--collect",
        "--property=Type=exec",
        "--property=KillMode=control-group",
        "--property=TimeoutStopSec=30s",
        "--property=SendSIGKILL=yes",
        "bash",
        "-lc",
        "sleep 2; systemctl reboot",
      ]),
      expect.any(Function),
    );
  });

  it("schedules shutdown through a transient systemd unit", async () => {
    await scheduleSystemShutdown();

    expect(execFileMock).toHaveBeenCalledWith(
      "systemd-run",
      expect.arrayContaining(["bash", "-lc", "sleep 2; systemctl poweroff"]),
      expect.any(Function),
    );
  });

  it("schedules factory reset via script file with env vars injected through --setenv", async () => {
    await scheduleFactoryReset();

    // execFileMock called twice: "which npm" then "systemd-run"
    expect(execFileMock).toHaveBeenCalledTimes(2);
    expect(execFileMock).toHaveBeenCalledWith(
      "which",
      ["npm"],
      expect.any(Function),
    );

    const systemdCall = execFileMock.mock.calls[1];
    expect(systemdCall?.[0]).toBe("systemd-run");

    const args: string[] = systemdCall?.[1] ?? [];

    // Systemd unit flags
    expect(args).toContain("--quiet");
    expect(args).toContain("--no-block");
    expect(args).toContain("--collect");
    expect(args).toContain("--property=Type=exec");

    // Environment variables injected via --setenv (no shell interpolation)
    const setenvPairs = args
      .map((arg, i) => (args[i - 1] === "--setenv" ? arg : null))
      .filter(Boolean) as string[];

    const envMap = Object.fromEntries(
      setenvPairs.map((pair) => {
        const eq = pair.indexOf("=");
        return [pair.slice(0, eq), pair.slice(eq + 1)];
      }),
    );

    expect(envMap.DATABASE_URL).toBe(serverEnvMock.DATABASE_URL);
    expect(envMap.HOMEIO_RESET_DATA_ROOT).toBe("/DATA");
    expect(envMap.HOMEIO_RESET_STACKS_ROOT).toBe("/DATA/Apps");
    expect(envMap.HOMEIO_RESET_WORKDIR).toBeTruthy();
    expect(envMap.HOMEIO_RESET_NPM_BIN).toBeTruthy();

    // Script path is the last argument — no inline bash command string
    const scriptArg = args.at(-1) ?? "";
    expect(scriptArg).toMatch(/factory-reset\.sh$/);
    expect(args).not.toContain("-lc");
  });

  it("rejects factory reset when the script does not exist", async () => {
    statMock.mockRejectedValueOnce(
      Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
    );

    await expect(scheduleFactoryReset()).rejects.toThrow(
      "Factory reset script not found",
    );
    expect(execFileMock).not.toHaveBeenCalledWith(
      "systemd-run",
      expect.anything(),
      expect.anything(),
    );
  });
});
bytes
4,749
lines
164
Type
ts
Latest commit
16 October 2026