/**
* Tests for segregated Docker service interfaces.
* Verifies Interface Segregation Principle (ISP) compliance.
*/
import { describe, expect, it, vi } from "vitest";
import type { HostConfig } from "../types.js";
import type {
IContainerOperations,
IDockerClientProvider,
IDockerSystemInfo,
IImageOperations,
IResourceOperations,
} from "./docker-interfaces.js";
describe("Docker Interface Segregation", () => {
const mockHost: HostConfig = {
name: "test-host",
host: "192.168.1.100",
protocol: "ssh" as const,
port: 22,
sshUser: "testuser",
};
describe("IContainerOperations", () => {
it("should only include container-related methods", () => {
// Create a mock that implements ONLY container operations
const containerOps: IContainerOperations = {
listContainers: vi.fn(),
containerAction: vi.fn(),
getContainerLogs: vi.fn(),
getContainerStats: vi.fn(),
execContainer: vi.fn(),
getContainerProcesses: vi.fn(),
inspectContainer: vi.fn(),
findContainerHost: vi.fn(),
recreateContainer: vi.fn(),
};
// Should have exactly 9 methods
expect(Object.keys(containerOps)).toHaveLength(9);
// Should not have image, resource, or system methods
expect(containerOps).not.toHaveProperty("pullImage");
expect(containerOps).not.toHaveProperty("listNetworks");
expect(containerOps).not.toHaveProperty("getDockerInfo");
});
it("should allow handlers to depend only on container operations", async () => {
// Simulates a container handler that only needs container operations
async function handleContainerList(
service: IContainerOperations,
hosts: HostConfig[]
): Promise<number> {
const containers = await service.listContainers(hosts);
return containers.length;
}
const mockService: IContainerOperations = {
listContainers: vi.fn().mockResolvedValue([{ id: "abc123", name: "test" }]),
containerAction: vi.fn(),
getContainerLogs: vi.fn(),
getContainerStats: vi.fn(),
execContainer: vi.fn(),
getContainerProcesses: vi.fn(),
inspectContainer: vi.fn(),
findContainerHost: vi.fn(),
recreateContainer: vi.fn(),
};
const count = await handleContainerList(mockService, [mockHost]);
expect(count).toBe(1);
expect(mockService.listContainers).toHaveBeenCalledWith([mockHost]);
});
});
describe("IImageOperations", () => {
it("should only include image-related methods", () => {
const imageOps: IImageOperations = {
listImages: vi.fn(),
pullImage: vi.fn(),
removeImage: vi.fn(),
buildImage: vi.fn(),
};
// Should have exactly 4 methods
expect(Object.keys(imageOps)).toHaveLength(4);
// Should not have container, resource, or system methods
expect(imageOps).not.toHaveProperty("listContainers");
expect(imageOps).not.toHaveProperty("listNetworks");
expect(imageOps).not.toHaveProperty("getDockerInfo");
});
it("should allow handlers to depend only on image operations", async () => {
async function handleImagePull(
service: IImageOperations,
image: string,
host: HostConfig
): Promise<string> {
const result = await service.pullImage(image, host);
return result.status;
}
const mockService: IImageOperations = {
listImages: vi.fn(),
pullImage: vi.fn().mockResolvedValue({ status: "Image pulled successfully" }),
removeImage: vi.fn(),
buildImage: vi.fn(),
};
const status = await handleImagePull(mockService, "nginx:latest", mockHost);
expect(status).toBe("Image pulled successfully");
expect(mockService.pullImage).toHaveBeenCalledWith("nginx:latest", mockHost);
});
});
describe("IResourceOperations", () => {
it("should only include resource management methods", () => {
const resourceOps: IResourceOperations = {
listNetworks: vi.fn(),
listVolumes: vi.fn(),
getDockerDiskUsage: vi.fn(),
pruneDocker: vi.fn(),
};
// Should have exactly 4 methods
expect(Object.keys(resourceOps)).toHaveLength(4);
// Should not have container, image, or system methods
expect(resourceOps).not.toHaveProperty("listContainers");
expect(resourceOps).not.toHaveProperty("pullImage");
expect(resourceOps).not.toHaveProperty("getDockerInfo");
});
it("should allow handlers to depend only on resource operations", async () => {
async function handleResourceCleanup(
service: IResourceOperations,
host: HostConfig
): Promise<number> {
const results = await service.pruneDocker(host, "all");
return results.length;
}
const mockService: IResourceOperations = {
listNetworks: vi.fn(),
listVolumes: vi.fn(),
getDockerDiskUsage: vi.fn(),
pruneDocker: vi
.fn()
.mockResolvedValue([{ type: "images", spaceReclaimed: 1024000, itemsDeleted: 5 }]),
};
const count = await handleResourceCleanup(mockService, mockHost);
expect(count).toBe(1);
expect(mockService.pruneDocker).toHaveBeenCalledWith(mockHost, "all");
});
});
describe("IDockerSystemInfo", () => {
it("should only include system information methods", () => {
const systemInfo: IDockerSystemInfo = {
getDockerInfo: vi.fn(),
getHostStatus: vi.fn(),
};
// Should have exactly 2 methods
expect(Object.keys(systemInfo)).toHaveLength(2);
// Should not have container, image, or resource methods
expect(systemInfo).not.toHaveProperty("listContainers");
expect(systemInfo).not.toHaveProperty("pullImage");
expect(systemInfo).not.toHaveProperty("listNetworks");
});
it("should allow handlers to depend only on system info", async () => {
async function handleStatusCheck(
service: IDockerSystemInfo,
hosts: HostConfig[]
): Promise<boolean> {
const statuses = await service.getHostStatus(hosts);
return statuses.every((s) => s.status === "online");
}
const mockService: IDockerSystemInfo = {
getDockerInfo: vi.fn(),
getHostStatus: vi.fn().mockResolvedValue([{ host: "test-host", status: "online" }]),
};
const allOnline = await handleStatusCheck(mockService, [mockHost]);
expect(allOnline).toBe(true);
expect(mockService.getHostStatus).toHaveBeenCalledWith([mockHost]);
});
});
describe("IDockerClientProvider", () => {
it("should only include client management methods", () => {
const clientProvider: IDockerClientProvider = {
getDockerClient: vi.fn(),
clearClients: vi.fn(),
};
// Should have exactly 2 methods
expect(Object.keys(clientProvider)).toHaveLength(2);
// Should not have any operational methods
expect(clientProvider).not.toHaveProperty("listContainers");
expect(clientProvider).not.toHaveProperty("pullImage");
});
it("should be used internally for low-level access", async () => {
const mockClient = {
listContainers: vi.fn(),
getContainer: vi.fn(),
};
const clientProvider: IDockerClientProvider = {
getDockerClient: vi.fn().mockResolvedValue(mockClient),
clearClients: vi.fn(),
};
const client = await clientProvider.getDockerClient(mockHost);
expect(client).toBe(mockClient);
expect(clientProvider.getDockerClient).toHaveBeenCalledWith(mockHost);
});
});
describe("Interface composition", () => {
it("should allow full service to implement all interfaces", () => {
// Simulates the DockerService facade implementing all interfaces
class MockFullService
implements
IContainerOperations,
IImageOperations,
IResourceOperations,
IDockerSystemInfo,
IDockerClientProvider
{
// Container operations
listContainers = vi.fn();
containerAction = vi.fn();
getContainerLogs = vi.fn();
getContainerStats = vi.fn();
execContainer = vi.fn();
getContainerProcesses = vi.fn();
inspectContainer = vi.fn();
findContainerHost = vi.fn();
recreateContainer = vi.fn();
// Image operations
listImages = vi.fn();
pullImage = vi.fn();
removeImage = vi.fn();
buildImage = vi.fn();
// Resource operations
listNetworks = vi.fn();
listVolumes = vi.fn();
getDockerDiskUsage = vi.fn();
pruneDocker = vi.fn();
// System info
getDockerInfo = vi.fn();
getHostStatus = vi.fn();
// Client provider
getDockerClient = vi.fn();
clearClients = vi.fn();
}
const service = new MockFullService();
// Should satisfy all interface types
const containerOps: IContainerOperations = service;
const imageOps: IImageOperations = service;
const resourceOps: IResourceOperations = service;
const systemInfo: IDockerSystemInfo = service;
const clientProvider: IDockerClientProvider = service;
expect(containerOps).toBeDefined();
expect(imageOps).toBeDefined();
expect(resourceOps).toBeDefined();
expect(systemInfo).toBeDefined();
expect(clientProvider).toBeDefined();
});
it("should allow specialized mocks for testing", () => {
// Test that only needs container operations can mock just that interface
function testContainerHandler(service: IContainerOperations): void {
service.listContainers([mockHost]);
}
const minimalMock: IContainerOperations = {
listContainers: vi.fn(),
// Other methods don't need to be implemented for this test
containerAction: vi.fn(),
getContainerLogs: vi.fn(),
getContainerStats: vi.fn(),
execContainer: vi.fn(),
getContainerProcesses: vi.fn(),
inspectContainer: vi.fn(),
findContainerHost: vi.fn(),
recreateContainer: vi.fn(),
};
testContainerHandler(minimalMock);
expect(minimalMock.listContainers).toHaveBeenCalled();
});
});
});