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
54 changes: 54 additions & 0 deletions apps/dokploy/__test__/settings/docker-server-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,60 @@ describe("settings Docker server boundary", () => {
);
});

it("logs who started a panel update", async () => {
mocks.getUpdateData.mockResolvedValue({
latestVersion: "off_v0.29.8/Fork_159+next",
updateAvailable: true,
updateSource: "agenthits",
latestOfficialVersion: "v0.30.0",
});
mocks.startServerUpdate.mockReturnValue(true);
const log = vi.spyOn(console, "log").mockImplementation(() => {});

await expect(createCaller("owner").updateServer()).resolves.toBe(true);

expect(log).toHaveBeenCalledWith(
"[panel] Update to off_v0.29.8/Fork_159+next requested by an unknown user (owner) through a browser session from an unknown address, user agent: none",
);
log.mockRestore();
});

it("does not log an update that did not start", async () => {
mocks.getUpdateData.mockResolvedValue({
latestVersion: "off_v0.29.8/Fork_159+next",
updateAvailable: true,
updateSource: "agenthits",
latestOfficialVersion: "v0.30.0",
});
mocks.startServerUpdate.mockReturnValue(false);
const log = vi.spyOn(console, "log").mockImplementation(() => {});

await expect(createCaller().updateServer()).resolves.toBe(true);

expect(log).not.toHaveBeenCalledWith(
expect.stringContaining("[panel] Update"),
);
log.mockRestore();
});

it("logs who reloaded the panel before it restarts", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
mocks.reloadDockerResource.mockImplementation(async () => {
expect(log).toHaveBeenCalledWith(
expect.stringMatching(/^\[panel\] Reload requested by .* \(owner\) /),
);
});

await expect(createCaller("owner").reloadServer()).resolves.toBe(true);

expect(mocks.reloadDockerResource).toHaveBeenCalledWith(
"dokploy",
undefined,
"v0.30.6",
);
log.mockRestore();
});

it("passes the image keep count to the AgentHits update command", async () => {
mocks.getUpdateData.mockResolvedValue({
latestVersion: "off_v0.29.8/Fork_159+next",
Expand Down
95 changes: 95 additions & 0 deletions apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ vi.mock("@dokploy/server/utils/process/execAsync", () => ({

const {
cleanupOldDokployImages,
getAgentHitsReloadCommand,
getAgentHitsUpdateCommand,
getDokployImageCleanupCommand,
getDokployImageKeepCount,
getDokployImageRepositories,
getDokployImages,
getOfficialUpdateCommand,
reloadDockerResource,
} = await import("@dokploy/server/services/settings");
const { UPDATE_IMAGE_PULLED_MARKER } = await import(
"@dokploy/server/services/web-server-update"
Expand Down Expand Up @@ -372,3 +374,96 @@ describe("Dokploy update commands", () => {
);
});
});

describe("Dokploy reload command", () => {
const DIGEST = `ghcr.io/agenthits/dokploy@sha256:${"b".repeat(64)}`;

const runReload = (env: Record<string, string>) => {
const dir = mkdtempSync(join(tmpdir(), "dokploy-reload-"));
const log = join(dir, "log");
try {
writeFileSync(
join(dir, "docker"),
`#!/bin/sh
echo "$*" >> "${log}"
case "$*" in
"ps -q"*) [ -n "$NO_TASK" ] || echo task1 ;;
"inspect --format {{.Image}} task1") echo sha256:cur ;;
"image inspect --format {{index .RepoDigests 0}} sha256:cur")
if [ -n "$NO_DIGEST" ]; then echo "index out of range: 0" >&2; exit 1; fi
echo "${DIGEST}" ;;
esac
`,
);
chmodSync(join(dir, "docker"), 0o755);
writeFileSync(log, "");
let status = 0;
try {
execFileSync("sh", ["-c", getAgentHitsReloadCommand()], {
env: {
...process.env,
...env,
PATH: `${dir}:${process.env.PATH}`,
},
stdio: "pipe",
});
} catch (error) {
status = (error as { status: number }).status;
}
return { status, calls: readFileSync(log, "utf8").trim().split("\n") };
} finally {
rmSync(dir, { recursive: true, force: true });
}
};

it(
"restarts the running build by digest without pulling",
() => {
const { status, calls } = runReload({});
expect(status).toBe(0);
expect(calls.some((call) => call.startsWith("pull"))).toBe(false);
expect(calls.at(-1)).toBe(
`service update --force --no-resolve-image --image ${DIGEST} --update-order stop-first --update-failure-action rollback dokploy`,
);
},
SPAWN_TEST_TIMEOUT_MS,
);

it(
"falls back to the image id for a build without a registry digest",
() => {
const { status, calls } = runReload({ NO_DIGEST: "1" });
expect(status).toBe(0);
expect(calls.at(-1)).toContain("--image sha256:cur ");
},
SPAWN_TEST_TIMEOUT_MS,
);

it(
"does not restart the panel when its running image is unknown",
() => {
const { status, calls } = runReload({ NO_TASK: "1" });
expect(status).toBe(1);
expect(calls.some((call) => call.startsWith("service update"))).toBe(
false,
);
},
SPAWN_TEST_TIMEOUT_MS,
);

it("reloads an AgentHits panel without the update command", async () => {
vi.stubEnv("DOKPLOY_UPDATE_SOURCE", "agenthits");
execAsync.mockReset();
execAsync
.mockResolvedValueOnce({ stdout: "service\n", stderr: "" })
.mockResolvedValueOnce({ stdout: "", stderr: "" });
try {
await reloadDockerResource("dokploy", undefined, "v0.30.6");

expect(execAsync).toHaveBeenLastCalledWith(getAgentHitsReloadCommand());
expect(execAsync.mock.lastCall?.[0]).not.toContain("docker pull");
} finally {
vi.unstubAllEnvs();
}
});
});
48 changes: 48 additions & 0 deletions apps/dokploy/__test__/settings/panel-restart-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { IncomingMessage } from "node:http";
import { afterEach, describe, expect, it, vi } from "vitest";

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

const { logPanelRestart } = await import(
"../../server/api/utils/panel-restart-log"
);

const request = (headers: Record<string, string>) =>
({ headers }) as unknown as IncomingMessage;

describe("panel restart log", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("names the user, the address and the browser", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});

logPanelRestart("Reload", {
req: request({
"x-forwarded-for": "198.51.100.7, 10.0.0.2",
"user-agent": "Mozilla/5.0 Test",
}),
user: { email: "owner@example.com", role: "owner" },
session: { authMethod: "session" },
});

expect(log).toHaveBeenCalledWith(
"[panel] Reload requested by owner@example.com (owner) through a browser session from 198.51.100.7, user agent: Mozilla/5.0 Test",
);
});

it("says when an API key started it", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});

logPanelRestart("Update to agenthits-v1.1.0", {
req: request({ "x-real-ip": "203.0.113.5" }),
user: { email: "ci@example.com", role: "admin" },
session: { authMethod: "api-key" },
});

expect(log).toHaveBeenCalledWith(
"[panel] Update to agenthits-v1.1.0 requested by ci@example.com (admin) through an API key from 203.0.113.5, user agent: none",
);
});
});
Loading