IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

disk-service.test.ts

Open raw
import { afterEach, describe, expect, it, vi, beforeEach } from "vitest";

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

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

vi.mock("node:fs/promises", () => ({
  readFile: readFileMock,
  writeFile: vi.fn(),
  mkdir: vi.fn(),
  stat: vi.fn(),
}));

import {
  getDiskInventory,
  invalidateDiskList,
  mountPartition,
  unmountPartition,
  wipeDisk,
} from "@/lib/server/modules/system/disk-service";

describe("disk-service safety validations", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    execFileMock.mockReset();
    readFileMock.mockReset();
  });

  describe("wipeDisk", () => {
    it("rejects invalid or dangerous device paths", async () => {
      await expect(wipeDisk("/dev/sda; rm -rf /")).rejects.toThrow("Invalid disk device path");
      await expect(wipeDisk("/dev/../etc/passwd")).rejects.toThrow("Invalid disk device path");
      await expect(wipeDisk("sda")).rejects.toThrow("Invalid disk device path");
      await expect(wipeDisk("/dev/sda1")).rejects.toThrow("Invalid disk device path");
    });

    it("rejects wiping a disk containing active mounts in /proc/mounts", async () => {
      readFileMock.mockResolvedValueOnce(
        "/dev/sdb1 /data ext4 rw,relatime 0 0\n" +
        "/dev/sda2 / ext4 rw,relatime 0 0\n"
      );

      await expect(wipeDisk("/dev/sdb")).rejects.toThrow("Cannot modify active mounted device");
    });

    it("rejects wiping a system disk mounted at /", async () => {
      readFileMock.mockResolvedValueOnce(
        "/dev/sda1 /boot/efi vfat rw 0 0\n" +
        "/dev/sda2 / ext4 rw,relatime 0 0\n"
      );

      await expect(wipeDisk("/dev/sda")).rejects.toThrow("Cannot modify system device containing critical mountpoint");
    });
  });

  describe("mountPartition", () => {
    it("rejects invalid partition device paths", async () => {
      await expect(mountPartition("/dev/sda", "/mnt/data"))
        .rejects.toThrow("Invalid partition device path");
      await expect(mountPartition("/dev/sda1; rm -rf /", "/mnt/data"))
        .rejects.toThrow("Invalid partition device path");
    });

    it("rejects mountpoints containing newlines or carriage returns", async () => {
      await expect(mountPartition("/dev/sdb1", "/mnt/data\n/dev/sdc1 /"))
        .rejects.toThrow("Mount point must be a safe, absolute path");
      await expect(mountPartition("/dev/sdb1", "/mnt/data\r\n"))
        .rejects.toThrow("Mount point must be a safe, absolute path");
      await expect(mountPartition("/dev/sdb1", "relative/path"))
        .rejects.toThrow("Mount point must be a safe, absolute path");
    });

    it("rejects dangerous system mountpoints", async () => {
      await expect(mountPartition("/dev/sdb1", "/"))
        .rejects.toThrow("Cannot mount partition over critical system directory: /");
      await expect(mountPartition("/dev/sdb1", "/etc"))
        .rejects.toThrow("Cannot mount partition over critical system directory: /etc");
    });

    it("only mounts inside /mnt, /media, /srv or /DATA", async () => {
      for (const mountPoint of ["/opt/data", "/etc/ssh", "/root/.ssh", "/usr/local/bin", "/mnt", "/DATA", "/mnt/../etc/x"]) {
        await expect(mountPartition("/dev/sdb1", mountPoint)).rejects.toThrow(/Mount point must be/);
      }
      expect(execFileMock).not.toHaveBeenCalled();
    });

    it("mounts inside an allowed folder, normalizing the path", async () => {
      execFileMock.mockImplementation((...args: unknown[]) => (args.at(-1) as (...a: unknown[]) => void)(null, "", ""));
      await mountPartition("/dev/sdb1", "/mnt//data/");
      expect(execFileMock).toHaveBeenCalledWith("mount", ["/dev/sdb1", "/mnt/data"], expect.any(Function));
    });
  });

  describe("unmountPartition", () => {
    it("rejects unmounting root or system directories", async () => {
      await expect(unmountPartition("/")).rejects.toThrow("Cannot unmount critical system directory: /");
      await expect(unmountPartition("/boot")).rejects.toThrow("Cannot unmount critical system directory: /boot");
    });
  });
});

describe("getDiskInventory", () => {
  const originalPlatform = process.platform;

  function setPlatform(platform: NodeJS.Platform) {
    Object.defineProperty(process, "platform", { value: platform, configurable: true });
  }

  beforeEach(() => {
    execFileMock.mockReset();
    invalidateDiskList();
    setPlatform("linux");
    vi.stubEnv("HOMEIO_CONTAINER", "");
  });

  afterEach(() => {
    setPlatform(originalPlatform);
    vi.unstubAllEnvs();
  });

  function lsblkReturns(blockdevices: unknown[]) {
    // promisify(execFile) resolves to { stdout, stderr }; the mock has no custom promisify.
    execFileMock.mockImplementation((...args: unknown[]) =>
      (args.at(-1) as (...a: unknown[]) => void)(null, { stdout: JSON.stringify({ blockdevices }), stderr: "" }),
    );
  }

  it("lists real disks and leaves out empty and in-memory devices", async () => {
    lsblkReturns([
      { name: "sda", size: 500_000_000_000, type: "disk", rm: false, rota: false, children: [] },
      { name: "nbd0", size: 0, type: "disk" },
      { name: "zram0", size: 1_000_000_000, type: "disk" },
      { name: "mmcblk0", size: 0, type: "disk" },
      { name: "loop3", size: 50_000_000, type: "loop" },
    ]);

    const inventory = await getDiskInventory();

    expect(inventory.disks.map((d) => d.name)).toEqual(["sda"]);
    expect(inventory.unavailableReason).toBeNull();
    expect(inventory.readOnly).toBe(false);
  });

  it("reuses the disk list between polls, and reads it again after a disk command", async () => {
    lsblkReturns([{ name: "sda", type: "disk", size: 500_000_000_000, children: [] }]);

    await getDiskInventory();
    await getDiskInventory();
    expect(execFileMock).toHaveBeenCalledTimes(1);

    await unmountPartition("/dev/sda1").catch(() => {});
    await getDiskInventory();
    const lsblkCalls = execFileMock.mock.calls.filter(([command]) => command === "lsblk");
    expect(lsblkCalls).toHaveLength(2);
  });

  it("says lsblk is missing instead of reporting no disks", async () => {
    execFileMock.mockImplementation((...args: unknown[]) =>
      (args.at(-1) as (...a: unknown[]) => void)(Object.assign(new Error("spawn lsblk ENOENT"), { code: "ENOENT" })),
    );

    const host = await getDiskInventory();
    expect(host.disks).toEqual([]);
    expect(host.unavailableReason).toContain("apt install util-linux");

    vi.stubEnv("HOMEIO_CONTAINER", "true");
    const container = await getDiskInventory();
    expect(container.unavailableReason).toContain("missing from this container image");
    expect(container.readOnly).toBe(true);
  });

  it("explains that disk management needs Linux", async () => {
    setPlatform("darwin");
    execFileMock.mockImplementation((...args: unknown[]) =>
      (args.at(-1) as (...a: unknown[]) => void)(Object.assign(new Error("spawn lsblk ENOENT"), { code: "ENOENT" })),
    );

    const inventory = await getDiskInventory();
    expect(inventory.unavailableReason).toBe("Disk management needs Linux; this machine runs darwin.");
  });
});
bytes
7,211
lines
187
Type
ts
Latest commit
16 October 2026