Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/dokploy/__test__/settings/docker-server-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const mocks = vi.hoisted(() => ({
generateOpenApiDocument: vi.fn(),
getAccessibleServerIds: vi.fn(),
getAgentHitsUpdateCommand: vi.fn(),
getAgentHitsUpdateImage: vi.fn(() => "update image"),
getOfficialUpdateCommand: vi.fn(),
getOfficialUpdateImage: vi.fn(() => "official image"),
getDockerDiskUsage: vi.fn(),
getDokployImageTag: vi.fn(),
getDokployVersionData: vi.fn(),
Expand Down Expand Up @@ -97,6 +99,8 @@ vi.mock("@dokploy/server", async () => ({
getAccessibleServerIds: mocks.getAccessibleServerIds,
getAgentHitsUpdateCommand: mocks.getAgentHitsUpdateCommand,
getOfficialUpdateCommand: mocks.getOfficialUpdateCommand,
getAgentHitsUpdateImage: mocks.getAgentHitsUpdateImage,
getOfficialUpdateImage: mocks.getOfficialUpdateImage,
getDockerDiskUsage: mocks.getDockerDiskUsage,
getDokployImageTag: mocks.getDokployImageTag,
getDokployVersionData: mocks.getDokployVersionData,
Expand Down Expand Up @@ -473,6 +477,7 @@ describe("settings Docker server boundary", () => {
);
expect(mocks.startServerUpdate).toHaveBeenCalledWith(
"agenthits update command",
"update image",
);
});

Expand Down Expand Up @@ -524,6 +529,7 @@ describe("settings Docker server boundary", () => {
);
expect(mocks.startServerUpdate).toHaveBeenCalledWith(
"official update command",
"official image",
);
expect(mocks.getAgentHitsUpdateCommand).not.toHaveBeenCalled();
});
Expand Down
136 changes: 136 additions & 0 deletions apps/dokploy/__test__/settings/engine-pull.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Readable } from "node:stream";
import type { EnginePullEvent } from "@dokploy/server/utils/docker/pull-progress";
import { describe, expect, it, vi } from "vitest";

vi.mock("@dokploy/server/constants", () => ({ docker: {} }));

const { pullImageThroughEngine } = await import(
"@dokploy/server/utils/docker/engine-pull"
);

const IMAGE = "registry.example.com/dokploy:test";

const recorded = (lines: string[]) =>
lines.map((line) => JSON.parse(line) as EnginePullEvent);

const RECORDED_PULL = recorded([
'{"status":"Pulling from example/dokploy","id":"test"}',
'{"status":"Pulling fs layer","progressDetail":{},"id":"4ac6ba6b019e"}',
'{"status":"Downloading","progressDetail":{"current":10000000,"total":40000000},"progress":"[=====> ] 10MB/40MB","id":"4ac6ba6b019e"}',
'{"status":"Download complete","progressDetail":{},"id":"4ac6ba6b019e"}',
'{"status":"Pull complete","progressDetail":{},"id":"4ac6ba6b019e"}',
'{"status":"Digest: sha256:0123456789abcdef"}',
'{"status":"Status: Downloaded newer image for registry.example.com/dokploy:test"}',
]);

const clientReplaying = (
events: EnginePullEvent[],
options: { pullError?: Error; streamError?: Error } = {},
) => ({
pull: vi.fn(async () => {
if (options.pullError) {
throw options.pullError;
}
return Readable.from([]);
}),
modem: {
followProgress: (
_stream: NodeJS.ReadableStream,
onFinished: (error: Error | null, output: EnginePullEvent[]) => void,
onProgress: (event: EnginePullEvent) => void,
) => {
for (const event of events) {
onProgress(event);
}
if (options.streamError) {
onFinished(options.streamError, []);
return;
}
onFinished(null, events);
},
},
});

describe("pullImageThroughEngine", () => {
it("passes each recorded event on and resolves when the stream ends", async () => {
const onEvent = vi.fn();
const client = clientReplaying(RECORDED_PULL);

await expect(
pullImageThroughEngine(IMAGE, onEvent, client),
).resolves.toBeUndefined();

expect(client.pull).toHaveBeenCalledWith(IMAGE);
expect(onEvent).toHaveBeenCalledTimes(RECORDED_PULL.length);
expect(onEvent).toHaveBeenNthCalledWith(3, RECORDED_PULL[2]);
});

it("rejects when the stream reports an error event", async () => {
const events = recorded([
'{"status":"Pulling fs layer","progressDetail":{},"id":"4ac6ba6b019e"}',
'{"errorDetail":{"message":"manifest unknown"},"error":"manifest unknown"}',
]);

await expect(
pullImageThroughEngine(IMAGE, vi.fn(), clientReplaying(events)),
).rejects.toThrow("manifest unknown");
});

it("rejects when the pull request itself fails", async () => {
const client = clientReplaying([], {
pullError: new Error("no such image"),
});

await expect(
pullImageThroughEngine(IMAGE, vi.fn(), client),
).rejects.toThrow("no such image");
});

it("gives up after 90 seconds without an event and destroys the stream", async () => {
vi.useFakeTimers();
try {
const stream = Readable.from([]);
const destroy = vi.spyOn(stream, "destroy");
let reportProgress: (event: EnginePullEvent) => void = () => {};
const client = {
pull: vi.fn(async () => stream),
modem: {
followProgress: (
_stream: NodeJS.ReadableStream,
_onFinished: unknown,
onProgress: (event: EnginePullEvent) => void,
) => {
reportProgress = onProgress;
},
},
};
const onEvent = vi.fn();
const outcome = expect(
pullImageThroughEngine(IMAGE, onEvent, client),
).rejects.toThrow("no pull progress for 90 seconds");

await vi.advanceTimersByTimeAsync(60_000);
reportProgress({ status: "Pulling fs layer" });
await vi.advanceTimersByTimeAsync(89_999);
expect(destroy).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);

await outcome;
expect(onEvent).toHaveBeenCalledTimes(1);
expect(destroy).toHaveBeenCalledOnce();
expect(vi.getTimerCount()).toBe(0);
} finally {
vi.useRealTimers();
}
});

it("rejects when the stream fails", async () => {
const client = clientReplaying(RECORDED_PULL, {
streamError: new Error("socket hang up"),
});

await expect(
pullImageThroughEngine(IMAGE, vi.fn(), client),
).rejects.toThrow("socket hang up");
});
});
Loading