homeio
PublicForked from doctor-io/homeio
disk-service.test.ts
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.");
});
});