import type Docker from "dockerode";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { HostConfig } from "../../types.js";
import { SystemService } from "./system-service.js";
import type { ClientManager } from "./utils/client-manager.js";
describe("SystemService", () => {
let service: SystemService;
let mockClientManager: ClientManager;
let mockDocker: Docker;
beforeEach(() => {
// Mock Docker client
mockDocker = {
info: vi.fn(),
version: vi.fn(),
df: vi.fn(),
listContainers: vi.fn(),
pruneContainers: vi.fn(),
pruneImages: vi.fn(),
pruneVolumes: vi.fn(),
pruneNetworks: vi.fn(),
pruneBuilder: vi.fn(),
} as unknown as Docker;
// Mock ClientManager
mockClientManager = {
getClient: vi.fn(() => mockDocker),
} as unknown as ClientManager;
service = new SystemService(mockClientManager);
});
describe("getHostStatus", () => {
it("should get status from multiple hosts", async (): Promise<void> => {
const hosts: HostConfig[] = [
{ name: "host1", host: "server1.example.com" },
{ name: "host2", host: "server2.example.com" },
];
(mockDocker.listContainers as ReturnType<typeof vi.fn>).mockResolvedValue([
{ State: "running" },
{ State: "exited" },
{ State: "running" },
]);
const result = await service.getHostStatus(hosts);
expect(result).toHaveLength(2);
expect(result[0].name).toBe("host1");
expect(result[0].connected).toBe(true);
expect(result[0].containerCount).toBe(3);
expect(result[0].runningCount).toBe(2);
});
it("should handle connection failures gracefully", async (): Promise<void> => {
const hosts: HostConfig[] = [{ name: "host1", host: "unreachable.example.com" }];
(mockDocker.listContainers as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Connection refused")
);
const result = await service.getHostStatus(hosts);
expect(result).toHaveLength(1);
expect(result[0].connected).toBe(false);
expect(result[0].error).toContain("Connection refused");
});
});
describe("getDockerInfo", () => {
it("should get Docker system info", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
(mockDocker.info as ReturnType<typeof vi.fn>).mockResolvedValue({
OperatingSystem: "Ubuntu 22.04",
Architecture: "x86_64",
KernelVersion: "5.15.0",
NCPU: 8,
MemTotal: 16000000000,
Driver: "overlay2",
DockerRootDir: "/var/lib/docker",
Containers: 10,
ContainersRunning: 5,
ContainersPaused: 1,
ContainersStopped: 4,
Images: 20,
});
(mockDocker.version as ReturnType<typeof vi.fn>).mockResolvedValue({
Version: "24.0.0",
ApiVersion: "1.43",
});
const result = await service.getDockerInfo(host);
expect(result.dockerVersion).toBe("24.0.0");
expect(result.os).toBe("Ubuntu 22.04");
expect(result.cpus).toBe(8);
expect(result.containersTotal).toBe(10);
});
});
describe("getDockerDiskUsage", () => {
it("should get Docker disk usage stats", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
(mockDocker.df as ReturnType<typeof vi.fn>).mockResolvedValue({
Images: [
{
Id: "img1",
Size: 100000000,
SharedSize: 50000000,
VirtualSize: 150000000,
Containers: 2,
},
{
Id: "img2",
Size: 200000000,
SharedSize: 0,
VirtualSize: 200000000,
Containers: 1,
},
],
Containers: [
{
Id: "cont1",
SizeRw: 10000000,
SizeRootFs: 110000000,
State: "running",
},
],
Volumes: [
{
Name: "vol1",
UsageData: {
Size: 50000000,
RefCount: 2,
},
},
],
BuildCache: [],
});
const result = await service.getDockerDiskUsage(host);
expect(result.images.total).toBe(2);
expect(result.images.active).toBe(2);
expect(result.containers.total).toBe(1);
expect(result.volumes.total).toBe(1);
});
});
describe("pruneDocker", () => {
it("should reject without force flag", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
await expect(service.pruneDocker(host, "images")).rejects.toThrow(
"pruneDocker requires { force: true }"
);
});
it("should reject with force: false", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
await expect(service.pruneDocker(host, "images", { force: false })).rejects.toThrow(
"pruneDocker requires { force: true }"
);
});
it("should prune images", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
(mockDocker.pruneImages as ReturnType<typeof vi.fn>).mockResolvedValue({
SpaceReclaimed: 100000000,
ImagesDeleted: [{ Deleted: "img1" }, { Deleted: "img2" }],
});
const result = await service.pruneDocker(host, "images", { force: true });
expect(result).toHaveLength(1);
expect(result[0].type).toBe("images");
expect(result[0].spaceReclaimed).toBe(100000000);
expect(result[0].itemsDeleted).toBe(2);
});
it("should prune all resources", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
(mockDocker.pruneContainers as ReturnType<typeof vi.fn>).mockResolvedValue({
SpaceReclaimed: 50000000,
ContainersDeleted: ["cont1"],
});
(mockDocker.pruneImages as ReturnType<typeof vi.fn>).mockResolvedValue({
SpaceReclaimed: 100000000,
ImagesDeleted: [{ Deleted: "img1" }],
});
(mockDocker.pruneVolumes as ReturnType<typeof vi.fn>).mockResolvedValue({
SpaceReclaimed: 75000000,
VolumesDeleted: ["vol1"],
});
(mockDocker.pruneNetworks as ReturnType<typeof vi.fn>).mockResolvedValue({
NetworksDeleted: ["net1"],
});
(mockDocker.pruneBuilder as ReturnType<typeof vi.fn>).mockResolvedValue({
SpaceReclaimed: 25000000,
CachesDeleted: ["cache1"],
});
const result = await service.pruneDocker(host, "all", { force: true });
expect(result).toHaveLength(5);
expect(result.find((r) => r.type === "containers")?.itemsDeleted).toBe(1);
expect(result.find((r) => r.type === "images")?.itemsDeleted).toBe(1);
expect(result.find((r) => r.type === "volumes")?.itemsDeleted).toBe(1);
});
it("should handle prune errors gracefully", async (): Promise<void> => {
const host: HostConfig = { name: "host1", host: "localhost" };
(mockDocker.pruneImages as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Prune failed")
);
const result = await service.pruneDocker(host, "images", { force: true });
expect(result).toHaveLength(1);
expect(result[0].type).toBe("images");
expect(result[0].spaceReclaimed).toBe(0);
expect(result[0].details).toContain("Error: Prune failed");
});
});
});