Skip to content
Open
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
91 changes: 91 additions & 0 deletions apps/dokploy/__test__/restore/web-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { restoreWebServerBackup } from "@dokploy/server/utils/restore/web-server";
import { beforeEach, describe, expect, it, vi } from "vitest";

const execAsyncMock = vi.fn();
const updateWebServerSettingsMock = vi.fn();
const getPublicIpWithFallbackMock = vi.fn();

vi.mock("@dokploy/server/utils/process/execAsync", () => ({
execAsync: (...args: unknown[]) => execAsyncMock(...args),
}));

vi.mock("@dokploy/server/services/web-server-settings", () => ({
updateWebServerSettings: (...args: unknown[]) =>
updateWebServerSettingsMock(...args),
}));

vi.mock("@dokploy/server/wss/utils", () => ({
getPublicIpWithFallback: (...args: unknown[]) =>
getPublicIpWithFallbackMock(...args),
}));

vi.mock("@dokploy/server/utils/backups/utils", () => ({
getS3Credentials: () => ["--s3-flag"],
}));

const destination = {
bucket: "my-bucket",
} as any;

describe("restoreWebServerBackup", () => {
beforeEach(() => {
vi.clearAllMocks();

// Default execAsync behaviour: the two `ls ... || true` probes must report
// that the database files exist, every other command resolves with no output.
execAsyncMock.mockImplementation((command: string) => {
if (command.includes("database.sql.gz")) {
return Promise.resolve({ stdout: "database.sql.gz", stderr: "" });
}
if (command.includes("ls") && command.includes("database.sql")) {
return Promise.resolve({ stdout: "database.sql", stderr: "" });
}
if (command.includes("docker ps")) {
return Promise.resolve({ stdout: "container-id", stderr: "" });
}
return Promise.resolve({ stdout: "", stderr: "" });
});
});

it("re-detects and persists the current public IP after a restore", async () => {
getPublicIpWithFallbackMock.mockResolvedValue("203.0.113.10");
const emit = vi.fn();

await restoreWebServerBackup(destination, "backup.zip", emit);

expect(getPublicIpWithFallbackMock).toHaveBeenCalledTimes(1);
expect(updateWebServerSettingsMock).toHaveBeenCalledWith({
serverIp: "203.0.113.10",
});
expect(emit).toHaveBeenCalledWith("Server IP updated to: 203.0.113.10");
expect(emit).toHaveBeenCalledWith("Restore completed successfully!");
});

it("does not overwrite the server IP when detection fails", async () => {
getPublicIpWithFallbackMock.mockResolvedValue(null);
const emit = vi.fn();

await restoreWebServerBackup(destination, "backup.zip", emit);

expect(getPublicIpWithFallbackMock).toHaveBeenCalledTimes(1);
expect(updateWebServerSettingsMock).not.toHaveBeenCalled();
expect(emit).toHaveBeenCalledWith(
"Warning: could not detect the public IP, the server IP was left unchanged. Update it manually in Web Server settings if needed.",
);
expect(emit).toHaveBeenCalledWith("Restore completed successfully!");
});

it("refreshes the IP only after the database restore has run", async () => {
getPublicIpWithFallbackMock.mockResolvedValue("203.0.113.10");
const emit = vi.fn();

await restoreWebServerBackup(destination, "backup.zip", emit);

// pg_restore must have been invoked before we touch the server IP.
const restoreCalled = execAsyncMock.mock.calls.some(([command]) =>
String(command).includes("pg_restore"),
);
expect(restoreCalled).toBe(true);
expect(updateWebServerSettingsMock).toHaveBeenCalledTimes(1);
});
});
16 changes: 16 additions & 0 deletions packages/server/src/utils/restore/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import type { Destination } from "@dokploy/server/services/destination";
import { updateWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
import { getS3Credentials } from "../backups/utils";
import { execAsync } from "../process/execAsync";

Expand Down Expand Up @@ -133,6 +135,20 @@ export const restoreWebServerBackup = async (
`docker exec ${postgresContainerId} rm /tmp/database.sql`,
);

// The restored database contains the server IP from the machine the
// backup was taken on. Re-detect the current host's public IP so the
// dashboard reflects this server instead of the old one.
emit("Refreshing server IP...");
const serverIp = await getPublicIpWithFallback();
if (serverIp) {
await updateWebServerSettings({ serverIp });
emit(`Server IP updated to: ${serverIp}`);
} else {
emit(
"Warning: could not detect the public IP, the server IP was left unchanged. Update it manually in Web Server settings if needed.",
);
}

emit("Restore completed successfully!");
} finally {
// Cleanup
Expand Down