Files
Checkmate/server/test/unit/providers/network/dockerProvider.test.ts
2026-06-09 12:34:30 -07:00

344 lines
13 KiB
TypeScript

import { describe, expect, it, jest } from "@jest/globals";
import { DockerProvider } from "../../../../src/service/network/DockerProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts";
import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
({
id: "mon-1",
teamId: "team-1",
type: "docker",
url: "my-container",
...overrides,
}) as Monitor;
const makeContainer = (overrides?: Record<string, any>) => ({
Id: "abc123def456abc123def456abc123def456abc123def456abc123def456abcd",
Names: ["/my-container"],
State: "running",
...overrides,
});
const createMockDocker = (containers: any[] = [makeContainer()], inspectResult?: any) => {
const inspect = jest.fn().mockResolvedValue(inspectResult ?? { State: { Status: "running" } });
const getContainer = jest.fn().mockReturnValue({ inspect });
const instance = {
listContainers: jest.fn().mockResolvedValue(containers),
getContainer,
};
const DockerLib = jest.fn().mockReturnValue(instance) as any;
return { DockerLib, instance, inspect, getContainer };
};
const createProvider = (opts?: { containers?: any[]; inspectResult?: any }) => {
const logger = createMockLogger();
const { DockerLib, instance, inspect, getContainer } = createMockDocker(opts?.containers, opts?.inspectResult);
const provider = new DockerProvider(logger as any, DockerLib);
return { provider, logger, docker: instance, inspect, getContainer };
};
// ── Contract ─────────────────────────────────────────────────────────────────
testStatusProviderContract("DockerProvider", {
create: () => createProvider().provider,
supportedType: "docker",
unsupportedType: "http",
makeMonitor: () => makeMonitor(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("DockerProvider", () => {
// ── Container matching ───────────────────────────────────────────────
describe("container matching", () => {
it("matches by exact container name", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "my-container" }));
expect(result.status).toBe(true);
expect(result.code).toBe(200);
});
it("matches by exact full ID (64 chars)", async () => {
const fullId = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd";
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: fullId }));
expect(result.status).toBe(true);
});
it("matches by partial ID prefix", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "abc123" }));
expect(result.status).toBe(true);
});
it("strips leading slashes from input", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "///my-container" }));
expect(result.status).toBe(true);
});
it("matches case-insensitively", async () => {
const { provider } = createProvider();
const result = await provider.handle(makeMonitor({ url: "MY-CONTAINER" }));
expect(result.status).toBe(true);
});
it("returns 404 when no container matches", async () => {
const { provider, logger } = createProvider();
const result = await provider.handle(makeMonitor({ url: "nonexistent" }));
expect(result.status).toBe(false);
expect(result.code).toBe(404);
expect(result.message).toBe("Docker container not found");
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("No container found"),
})
);
});
});
// ── Ambiguity detection ──────────────────────────────────────────────
describe("ambiguity detection", () => {
it("returns error when input matches both exact name and partial ID", async () => {
// Container whose name matches the input AND whose ID starts with the input
const container = makeContainer({
Id: "my-containerabc123def456abc123def456abc123def456abc123def456abcd",
Names: ["/my-container"],
});
const { provider, logger } = createProvider({ containers: [container] });
const result = await provider.handle(makeMonitor({ url: "my-container" }));
expect(result.status).toBe(false);
expect(result.code).toBe(NETWORK_ERROR);
expect(result.message).toContain("Ambiguous");
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("Ambiguous"),
})
);
});
it("reports 'exact ID' in ambiguity message when exact ID and name both match", async () => {
const fullId = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd";
const container = makeContainer({
Id: fullId,
Names: ["/" + fullId],
});
const { provider } = createProvider({ containers: [container] });
const result = await provider.handle(makeMonitor({ url: fullId }));
expect(result.message).toContain("Using exact ID");
});
it("reports 'exact name' in ambiguity message when name and partial ID match", async () => {
const container = makeContainer({
Id: "my-containerabc123def456abc123def456abc123def456abc123def456abcd",
Names: ["/my-container"],
});
const { provider } = createProvider({ containers: [container] });
const result = await provider.handle(makeMonitor({ url: "my-container" }));
expect(result.message).toContain("Using exact name");
});
});
// ── Container inspection ─────────────────────────────────────────────
describe("container inspection", () => {
it("returns running status for running container", async () => {
const { provider } = createProvider({ inspectResult: { State: { Status: "running" } } });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(true);
expect(result.message).toBe("Docker container status fetched successfully");
});
it("returns not-running status for stopped container", async () => {
const { provider } = createProvider({ inspectResult: { State: { Status: "exited" } } });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("returns NETWORK_ERROR when inspect response is null", async () => {
const logger = createMockLogger();
const inspect = jest.fn().mockResolvedValue(null);
const getContainer = jest.fn().mockReturnValue({ inspect });
const instance = { listContainers: jest.fn().mockResolvedValue([makeContainer()]), getContainer };
const DockerLib = jest.fn().mockReturnValue(instance) as any;
const provider = new DockerProvider(logger as any, DockerLib);
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.code).toBe(NETWORK_ERROR);
expect(result.message).toBe("No response from Docker container inspect");
});
it("returns false status when inspect response has no State", async () => {
const { provider } = createProvider({ inspectResult: {} });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("returns false status when inspect response has State but no Status", async () => {
const { provider } = createProvider({ inspectResult: { State: {} } });
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
});
it("handles inspect error with Docker-specific error", async () => {
const { provider } = createProvider();
// Make inspect throw a Docker error
const dockerErr = Object.assign(new Error("container not found"), {
statusCode: 404,
reason: "no such container",
json: { message: "Container not found" },
});
provider["docker"] = {
listContainers: jest.fn().mockResolvedValue([makeContainer()]),
getContainer: jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
}),
} as any;
// Need to re-assign; simpler to use the createProvider mock
const { provider: p2 } = createProvider();
// Override getContainer to return failing inspect
p2["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await p2.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.code).toBe(404);
expect(result.message).toBe("Container not found");
});
it("handles inspect error with Docker error using reason fallback", async () => {
const { provider } = createProvider();
const dockerErr = Object.assign(new Error("error"), {
statusCode: 500,
reason: "internal error",
json: {},
});
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("internal error");
});
it("handles inspect error with Docker error using Error message fallback", async () => {
const { provider } = createProvider();
const dockerErr = Object.assign(new Error("base error"), {
statusCode: 503,
reason: undefined,
json: undefined,
});
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await provider.handle(makeMonitor());
expect(result.message).toBe("base error");
});
it("handles inspect error with NETWORK_ERROR when no statusCode", async () => {
const { provider } = createProvider();
const dockerErr = Object.assign(new Error("unknown"), {
statusCode: undefined,
});
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(dockerErr),
});
const result = await provider.handle(makeMonitor());
expect(result.code).toBe(NETWORK_ERROR);
});
it("handles inspect error with standard Error (not Docker error)", async () => {
const { provider } = createProvider();
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue(new Error("ECONNREFUSED")),
});
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.message).toBe("ECONNREFUSED");
});
it("handles inspect error with non-Error non-Docker thrown value", async () => {
const { provider } = createProvider();
provider["docker"].getContainer = jest.fn().mockReturnValue({
inspect: jest.fn().mockRejectedValue("string error"),
});
const result = await provider.handle(makeMonitor());
expect(result.status).toBe(false);
expect(result.message).toBe("Failed to fetch Docker container information");
});
});
// ── Outer catch ──────────────────────────────────────────────────────
describe("outer error handling", () => {
it("throws AppError when containerInput is missing", async () => {
const { provider } = createProvider();
await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("Container name or ID is required for Docker monitor");
});
it("throws AppError when listContainers throws", async () => {
const { provider } = createProvider();
provider["docker"].listContainers = jest.fn().mockRejectedValue(new Error("Docker daemon unavailable"));
await expect(provider.handle(makeMonitor())).rejects.toThrow("Docker daemon unavailable");
});
it("throws AppError with default message for non-Error thrown values", async () => {
const { provider } = createProvider();
provider["docker"].listContainers = jest.fn().mockRejectedValue(null);
await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing Docker request");
});
});
});