mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 14:15:21 +02:00
feat(tests): add real tests for volume backup and restore functionality
This commit is contained in:
324
apps/dokploy/__test__/volume-backups/volume-backup.real.test.ts
Normal file
324
apps/dokploy/__test__/volume-backups/volume-backup.real.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const REAL_TEST_TIMEOUT = 300000;
|
||||
|
||||
// Mock ONLY database and notifications
|
||||
vi.mock("@dokploy/server/db", () => ({
|
||||
db: {
|
||||
query: { volumeBackups: { findFirst: vi.fn() } },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/volume-backups", () => ({
|
||||
findVolumeBackupById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/destination", () => ({
|
||||
findDestinationById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/application", () => ({
|
||||
findApplicationById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/deployment", () => ({
|
||||
createDeploymentVolumeBackup: vi.fn(),
|
||||
updateDeploymentStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
import type * as volumeBackupService from "@dokploy/server/services/volume-backups";
|
||||
import { backupVolume } from "@dokploy/server/utils/volume-backups/backup";
|
||||
|
||||
type VolumeBackupData = Awaited<
|
||||
ReturnType<typeof volumeBackupService.findVolumeBackupById>
|
||||
>;
|
||||
|
||||
const createMockDestination = () => ({
|
||||
destinationId: "test-dest",
|
||||
bucket: "test-bucket",
|
||||
accessKey: "key",
|
||||
secretAccessKey: "secret",
|
||||
region: "us-east-1",
|
||||
endpoint: "s3.amazonaws.com",
|
||||
});
|
||||
|
||||
const createMockVolumeBackup = (
|
||||
volumeName: string,
|
||||
appName: string,
|
||||
): Partial<VolumeBackupData> => ({
|
||||
volumeBackupId: "id",
|
||||
name: "Test",
|
||||
volumeName,
|
||||
appName,
|
||||
serviceType: "application",
|
||||
turnOff: false,
|
||||
prefix: "backups/",
|
||||
destination: createMockDestination() as any,
|
||||
application: { appName, serverId: null } as any,
|
||||
compose: null,
|
||||
});
|
||||
|
||||
async function cleanupDocker(volumeName: string) {
|
||||
try {
|
||||
await execAsync(`docker volume rm ${volumeName} 2>/dev/null || true`);
|
||||
console.log(`✅ Cleaned up volume: ${volumeName}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupFiles(appName: string) {
|
||||
try {
|
||||
const { LOGS_PATH, VOLUME_BACKUPS_PATH } = paths(false);
|
||||
|
||||
// Clean logs
|
||||
const logPath = path.join(LOGS_PATH, appName);
|
||||
await execAsync(`rm -rf "${logPath}" 2>/dev/null || true`);
|
||||
|
||||
// Clean volume backups directory
|
||||
const backupPath = path.join(VOLUME_BACKUPS_PATH, appName);
|
||||
await execAsync(`rm -rf "${backupPath}" 2>/dev/null || true`);
|
||||
|
||||
console.log(`✅ Cleaned up files for ${appName}`);
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Error during cleanup for ${appName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
describe(
|
||||
"Volume Backups - REAL Tests",
|
||||
() => {
|
||||
let currentVolumeName: string;
|
||||
let currentAppName: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
currentVolumeName = `test-vol-${Date.now()}`;
|
||||
currentAppName = `test-backup-${Date.now()}`;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
console.log(`\n🧹 Cleanup: ${currentVolumeName}`);
|
||||
await cleanupDocker(currentVolumeName);
|
||||
await cleanupFiles(currentAppName);
|
||||
|
||||
// Clean all test volumes
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`docker volume ls -q --filter "name=test-vol-" || true`,
|
||||
);
|
||||
if (stdout.trim()) {
|
||||
for (const vol of stdout.trim().split("\n")) {
|
||||
await execAsync(`docker volume rm ${vol} 2>/dev/null || true`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Clean all test backup directories
|
||||
try {
|
||||
const { VOLUME_BACKUPS_PATH } = paths(false);
|
||||
await execAsync(
|
||||
`find "${VOLUME_BACKUPS_PATH}" -maxdepth 1 -type d -name "test-*" -exec rm -rf {} + 2>/dev/null || true`,
|
||||
);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
console.log("✅ Cleanup done\n");
|
||||
});
|
||||
|
||||
it(
|
||||
"should backup volume with tar ",
|
||||
async () => {
|
||||
console.log(`\n🚀 Test backup: ${currentVolumeName}`);
|
||||
|
||||
// Create volume with data
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
echo 'test' > /data/file.txt
|
||||
mkdir -p /data/dir
|
||||
echo 'nested' > /data/dir/nested.txt
|
||||
"
|
||||
`);
|
||||
console.log("✅ Volume created with data");
|
||||
|
||||
// Backup using tar (simulating what backupVolume does)
|
||||
const backupVol = `backup-${Date.now()}`;
|
||||
await execAsync(`docker volume create ${backupVol}`);
|
||||
|
||||
try {
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/source -v ${backupVol}:/backup ubuntu bash -c "
|
||||
cd /source && tar cf /backup/test.tar .
|
||||
"
|
||||
`);
|
||||
console.log("✅ Backup created");
|
||||
|
||||
// Verify tar contains files
|
||||
const { stdout } = await execAsync(`
|
||||
docker run --rm -v ${backupVol}:/backup ubuntu tar -tf /backup/test.tar
|
||||
`);
|
||||
expect(stdout).toContain("file.txt");
|
||||
expect(stdout).toContain("dir/nested.txt");
|
||||
console.log("✅ Backup verified");
|
||||
} finally {
|
||||
await execAsync(`docker volume rm ${backupVol} 2>/dev/null || true`);
|
||||
}
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should verify backup command has proper logging",
|
||||
async () => {
|
||||
console.log(`\n🚀 Test logging: ${currentVolumeName}`);
|
||||
|
||||
const mock = createMockVolumeBackup(currentVolumeName, currentAppName);
|
||||
const command = await backupVolume(mock);
|
||||
|
||||
// Verify logging messages for Issue #3301
|
||||
expect(command).toContain("Volume name:");
|
||||
expect(command).toContain("Starting volume backup");
|
||||
expect(command).toContain("Volume backup done ✅");
|
||||
expect(command).toContain("Upload to S3 done ✅");
|
||||
expect(command).toContain("tar cvf");
|
||||
|
||||
console.log("✅ All log messages present");
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should backup 1GB volume using real backupVolume - Issue #3301",
|
||||
async () => {
|
||||
console.log(
|
||||
`\n🚀 Test 1GB backup with real code: ${currentVolumeName}`,
|
||||
);
|
||||
|
||||
// Create volume with ~1GB of data
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
console.log("✅ Volume created");
|
||||
|
||||
const startTime = Date.now();
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
echo 'Creating 1GB of test data...'
|
||||
dd if=/dev/zero of=/data/large-file-1.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-2.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-3.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-4.dat bs=1M count=250 2>/dev/null
|
||||
mkdir -p /data/metadata
|
||||
echo 'Large backup test - Issue 3301' > /data/metadata/info.txt
|
||||
echo 'marker-67890' > /data/metadata/marker.txt
|
||||
du -sh /data
|
||||
ls -lh /data
|
||||
"
|
||||
`);
|
||||
const createTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Created 1GB data in ${createTime}s`);
|
||||
|
||||
// Create backup directory (simulating what Dokploy does)
|
||||
const { VOLUME_BACKUPS_PATH } = paths(false);
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, currentAppName);
|
||||
await execAsync(`mkdir -p "${volumeBackupPath}"`);
|
||||
|
||||
// Use the REAL backupVolume function to generate the command
|
||||
const mock = createMockVolumeBackup(currentVolumeName, currentAppName);
|
||||
const fullCommand = await backupVolume(mock);
|
||||
|
||||
console.log("📦 Executing REAL Dokploy backupVolume() command...");
|
||||
|
||||
// Execute the REAL command (without S3 upload part)
|
||||
const backupStartTime = Date.now();
|
||||
const backupFileName = `${currentVolumeName}-${new Date().toISOString()}.tar`;
|
||||
|
||||
// Extract and execute just the backup part of the command (tar creation)
|
||||
// This is what Dokploy really does
|
||||
const commandWithoutS3 = fullCommand.replace(
|
||||
/rclone copyto[^\n]+/g,
|
||||
'echo "Skipping S3 upload - keeping file locally for test"',
|
||||
);
|
||||
|
||||
// Also prevent the cleanup of the backup file so we can verify it
|
||||
const commandWithoutCleanup = commandWithoutS3.replace(
|
||||
/rm "[^"]+\.tar"/g,
|
||||
'echo "Skipping cleanup for test verification"',
|
||||
);
|
||||
|
||||
try {
|
||||
// Execute the real Dokploy backup command
|
||||
await execAsync(commandWithoutCleanup);
|
||||
const backupTime = ((Date.now() - backupStartTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Backup executed in ${backupTime}s`);
|
||||
} catch (error: any) {
|
||||
console.error("Backup command failed:", error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Verify the backup file was actually created by Dokploy's command
|
||||
const { stdout: files } = await execAsync(
|
||||
`ls -lh "${volumeBackupPath}"`,
|
||||
);
|
||||
console.log(`Files in backup directory:\n${files}`);
|
||||
|
||||
// Find the actual backup file created
|
||||
const { stdout: backupFiles } = await execAsync(
|
||||
`find "${volumeBackupPath}" -name "*.tar" -type f`,
|
||||
);
|
||||
const backupFilePath = backupFiles.trim().split("\n")[0];
|
||||
|
||||
expect(existsSync(backupFilePath)).toBe(true);
|
||||
console.log(`✅ Backup file created: ${path.basename(backupFilePath)}`);
|
||||
|
||||
// Verify file size
|
||||
const { stdout: statOutput } = await execAsync(
|
||||
`stat -f%z "${backupFilePath}" 2>/dev/null || stat -c%s "${backupFilePath}"`,
|
||||
);
|
||||
|
||||
const sizeInMB = Number(statOutput.trim()) / (1024 * 1024);
|
||||
expect(sizeInMB).toBeGreaterThan(1000); // Should be > 1GB
|
||||
console.log(`✅ Backup file size: ${sizeInMB.toFixed(2)}MB`);
|
||||
|
||||
// Verify tar contents - this proves the backup worked
|
||||
const { stdout: tarContents } = await execAsync(
|
||||
`tar -tf "${backupFilePath}" | head -20`,
|
||||
);
|
||||
expect(tarContents).toContain("large-file-1.dat");
|
||||
expect(tarContents).toContain("large-file-2.dat");
|
||||
expect(tarContents).toContain("large-file-3.dat");
|
||||
expect(tarContents).toContain("large-file-4.dat");
|
||||
expect(tarContents).toContain("metadata/info.txt");
|
||||
expect(tarContents).toContain("metadata/marker.txt");
|
||||
|
||||
// Extract and verify one file to ensure data integrity
|
||||
const tempDir = path.join(volumeBackupPath, "temp-extract");
|
||||
await execAsync(`mkdir -p "${tempDir}"`);
|
||||
await execAsync(
|
||||
`tar -xf "${backupFilePath}" -C "${tempDir}" metadata/marker.txt`,
|
||||
);
|
||||
const { stdout: markerContent } = await execAsync(
|
||||
`cat "${tempDir}/metadata/marker.txt"`,
|
||||
);
|
||||
expect(markerContent.trim()).toBe("marker-67890");
|
||||
await execAsync(`rm -rf "${tempDir}"`);
|
||||
console.log("✅ Data integrity verified");
|
||||
|
||||
console.log("\n📊 Performance Summary:");
|
||||
console.log(` - Data creation: ${createTime}s`);
|
||||
console.log(` - Size: ${sizeInMB.toFixed(2)}MB`);
|
||||
console.log(
|
||||
"✅ 1GB backup test PASSED - Real Dokploy backupVolume() works correctly",
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
407
apps/dokploy/__test__/volume-backups/volume-restore.real.test.ts
Normal file
407
apps/dokploy/__test__/volume-backups/volume-restore.real.test.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const REAL_TEST_TIMEOUT = 300000;
|
||||
|
||||
// Mock ONLY database and services
|
||||
vi.mock("@dokploy/server/db", () => ({ db: {} }));
|
||||
|
||||
vi.mock("@dokploy/server/services/destination", () => ({
|
||||
findDestinationById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/application", () => ({
|
||||
findApplicationById: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as applicationService from "@dokploy/server/services/application";
|
||||
import * as destinationService from "@dokploy/server/services/destination";
|
||||
import { restoreVolume } from "@dokploy/server/utils/volume-backups/restore";
|
||||
|
||||
const createMockDestination = () => ({
|
||||
destinationId: "test-dest",
|
||||
bucket: "test-bucket",
|
||||
accessKey: "key",
|
||||
secretAccessKey: "secret",
|
||||
region: "us-east-1",
|
||||
endpoint: "s3.amazonaws.com",
|
||||
});
|
||||
|
||||
const createMockApplication = (appName: string) => ({
|
||||
applicationId: "test-app",
|
||||
appName,
|
||||
serverId: null,
|
||||
});
|
||||
|
||||
async function cleanupDocker(volumeName: string, containerName?: string) {
|
||||
try {
|
||||
if (containerName) {
|
||||
await execAsync(`docker stop ${containerName} 2>/dev/null || true`);
|
||||
await execAsync(`docker rm ${containerName} 2>/dev/null || true`);
|
||||
}
|
||||
await execAsync(`docker volume rm ${volumeName} 2>/dev/null || true`);
|
||||
console.log(`✅ Cleaned up: ${volumeName}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupFiles(appName: string) {
|
||||
try {
|
||||
const { LOGS_PATH, VOLUME_BACKUPS_PATH } = paths(false);
|
||||
|
||||
// Clean logs
|
||||
const logPath = path.join(LOGS_PATH, appName);
|
||||
await execAsync(`rm -rf "${logPath}" 2>/dev/null || true`);
|
||||
|
||||
// Clean volume backups directory
|
||||
const backupPath = path.join(VOLUME_BACKUPS_PATH, appName);
|
||||
await execAsync(`rm -rf "${backupPath}" 2>/dev/null || true`);
|
||||
|
||||
console.log(`✅ Cleaned up files for ${appName}`);
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Error during cleanup for ${appName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
describe(
|
||||
"Volume Restore - REAL Tests",
|
||||
() => {
|
||||
let currentVolumeName: string;
|
||||
let currentAppName: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
currentVolumeName = `test-vol-${Date.now()}`;
|
||||
currentAppName = `test-restore-${Date.now()}`;
|
||||
|
||||
vi.mocked(destinationService.findDestinationById).mockResolvedValue(
|
||||
createMockDestination() as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
createMockApplication(currentAppName) as any,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
console.log(`\n🧹 Cleanup: ${currentVolumeName}`);
|
||||
await cleanupDocker(currentVolumeName);
|
||||
await cleanupFiles(currentAppName);
|
||||
|
||||
// Clean all test volumes
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`docker volume ls -q --filter "name=test-vol-" || true`,
|
||||
);
|
||||
if (stdout.trim()) {
|
||||
for (const vol of stdout.trim().split("\n")) {
|
||||
await execAsync(`docker volume rm ${vol} 2>/dev/null || true`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Clean all test backup directories
|
||||
try {
|
||||
const { VOLUME_BACKUPS_PATH } = paths(false);
|
||||
await execAsync(
|
||||
`find "${VOLUME_BACKUPS_PATH}" -maxdepth 1 -type d -name "test-*" -exec rm -rf {} + 2>/dev/null || true`,
|
||||
);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
console.log("✅ Cleanup done\n");
|
||||
});
|
||||
|
||||
it(
|
||||
"should restore volume using real restoreVolume ",
|
||||
async () => {
|
||||
console.log(`\n🚀 Test restore with real code: ${currentVolumeName}`);
|
||||
|
||||
// Step 1: Create volume with data
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
echo 'original' > /data/original.txt
|
||||
mkdir -p /data/subdir
|
||||
echo 'nested' > /data/subdir/file.txt
|
||||
"
|
||||
`);
|
||||
console.log("✅ Created volume with data");
|
||||
|
||||
// Step 2: Create backup in the correct path (simulating what Dokploy does)
|
||||
const { VOLUME_BACKUPS_PATH } = paths(false);
|
||||
const volumeBackupPath = path.join(
|
||||
VOLUME_BACKUPS_PATH,
|
||||
currentVolumeName,
|
||||
);
|
||||
await execAsync(`mkdir -p "${volumeBackupPath}"`);
|
||||
|
||||
const backupFileName = `${currentVolumeName}-2024-01-01T00:00:00.000Z.tar`;
|
||||
const backupFilePath = path.join(volumeBackupPath, backupFileName);
|
||||
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/volume_data -v "${volumeBackupPath}":/backup ubuntu bash -c "
|
||||
cd /volume_data && tar cf /backup/${backupFileName} .
|
||||
"
|
||||
`);
|
||||
console.log("✅ Backup created");
|
||||
|
||||
// Step 3: Remove original volume
|
||||
await execAsync(`docker volume rm ${currentVolumeName}`);
|
||||
console.log("✅ Removed original");
|
||||
|
||||
// Step 4: Use REAL restoreVolume function to generate the command
|
||||
const mockDestination = createMockDestination();
|
||||
const mockApplication = createMockApplication(currentAppName);
|
||||
|
||||
vi.mocked(destinationService.findDestinationById).mockResolvedValue(
|
||||
mockDestination as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
mockApplication as any,
|
||||
);
|
||||
|
||||
const fullCommand = await restoreVolume(
|
||||
mockApplication.applicationId,
|
||||
mockDestination.destinationId,
|
||||
currentVolumeName,
|
||||
backupFileName,
|
||||
"",
|
||||
"application",
|
||||
);
|
||||
|
||||
console.log("📥 Executing REAL Dokploy restore command...");
|
||||
|
||||
// Execute the REAL command, but skip S3 download since we already have the file
|
||||
// The command checks if volume exists, handles containers using it, and restores
|
||||
const restoreStartTime = Date.now();
|
||||
|
||||
// Simulate the restore scenario: volume doesn't exist yet
|
||||
// The command will create it and restore from the backup file
|
||||
const commandWithoutS3 = fullCommand.replace(
|
||||
/rclone copyto[^\n]+/g,
|
||||
'echo "Skipping S3 download - file already present for test"',
|
||||
);
|
||||
|
||||
try {
|
||||
await execAsync(commandWithoutS3);
|
||||
const restoreTime = ((Date.now() - restoreStartTime) / 1000).toFixed(
|
||||
2,
|
||||
);
|
||||
console.log(`✅ Restore command executed in ${restoreTime}s`);
|
||||
} catch (error: any) {
|
||||
console.error("Restore command failed:", error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Step 5: Verify data integrity - this proves the restore worked
|
||||
const { stdout } = await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
cat /data/original.txt && cat /data/subdir/file.txt
|
||||
"
|
||||
`);
|
||||
expect(stdout).toContain("original");
|
||||
expect(stdout).toContain("nested");
|
||||
console.log("✅ Data integrity verified after real restore");
|
||||
|
||||
// Verify the volume actually exists
|
||||
const { stdout: volumeCheck } = await execAsync(
|
||||
`docker volume ls --filter name=${currentVolumeName} --format "{{.Name}}"`,
|
||||
);
|
||||
expect(volumeCheck.trim()).toBe(currentVolumeName);
|
||||
|
||||
console.log(
|
||||
"✅ Restore test PASSED - Real Dokploy restoreVolume() works correctly",
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should detect volume in use during restore",
|
||||
async () => {
|
||||
console.log(`\n🚀 Test error detection: ${currentVolumeName}`);
|
||||
|
||||
// Create volume with container using it
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
|
||||
const containerName = `test-container-${Date.now()}`;
|
||||
await execAsync(`
|
||||
docker run -d --name ${containerName} -v ${currentVolumeName}:/data ubuntu sleep 300
|
||||
`);
|
||||
console.log("✅ Container using volume");
|
||||
|
||||
try {
|
||||
// Generate restore command
|
||||
const command = await restoreVolume(
|
||||
"test-app",
|
||||
"test-dest",
|
||||
currentVolumeName,
|
||||
"backup.tar",
|
||||
"",
|
||||
"application",
|
||||
);
|
||||
|
||||
// Verify error detection messages for Issue #3301
|
||||
expect(command).toContain("CONTAINERS_USING_VOLUME=");
|
||||
expect(command).toContain(
|
||||
"Cannot restore volume as it is currently in use",
|
||||
);
|
||||
expect(command).toContain("Volume restore aborted");
|
||||
expect(command).toContain("Stop all containers/services");
|
||||
|
||||
console.log("✅ Error detection verified");
|
||||
} finally {
|
||||
await execAsync(`docker stop ${containerName} 2>/dev/null || true`);
|
||||
await execAsync(`docker rm ${containerName} 2>/dev/null || true`);
|
||||
}
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should restore 1GB volume using real restoreVolume",
|
||||
async () => {
|
||||
console.log(
|
||||
`\n🚀 Test 1GB restore with real code: ${currentVolumeName}`,
|
||||
);
|
||||
|
||||
// Step 1: Create volume with ~1GB of data
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
console.log("✅ Volume created");
|
||||
|
||||
const startTime = Date.now();
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
echo 'Creating 1GB of test data...'
|
||||
dd if=/dev/zero of=/data/large-file-1.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-2.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-3.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-4.dat bs=1M count=250 2>/dev/null
|
||||
mkdir -p /data/metadata
|
||||
echo 'Large restore test - Issue 3301' > /data/metadata/info.txt
|
||||
echo 'test-marker-12345' > /data/metadata/marker.txt
|
||||
du -sh /data
|
||||
"
|
||||
`);
|
||||
const createTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Created 1GB data in ${createTime}s`);
|
||||
|
||||
// Step 2: Create backup in the correct path
|
||||
const { VOLUME_BACKUPS_PATH } = paths(false);
|
||||
const volumeBackupPath = path.join(
|
||||
VOLUME_BACKUPS_PATH,
|
||||
currentVolumeName,
|
||||
);
|
||||
await execAsync(`mkdir -p "${volumeBackupPath}"`);
|
||||
|
||||
const backupFileName = `${currentVolumeName}-large.tar`;
|
||||
const backupFilePath = path.join(volumeBackupPath, backupFileName);
|
||||
|
||||
const backupStartTime = Date.now();
|
||||
console.log("📦 Creating backup of 1GB volume...");
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/volume_data -v "${volumeBackupPath}":/backup ubuntu bash -c "
|
||||
cd /volume_data && tar cf /backup/${backupFileName} .
|
||||
"
|
||||
`);
|
||||
const backupTime = ((Date.now() - backupStartTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Backup created in ${backupTime}s`);
|
||||
|
||||
// Step 3: Remove original volume
|
||||
await execAsync(`docker volume rm ${currentVolumeName}`);
|
||||
console.log("✅ Removed original 1GB volume");
|
||||
|
||||
// Step 4: Use REAL restoreVolume function
|
||||
const mockDestination = createMockDestination();
|
||||
const mockApplication = createMockApplication(currentAppName);
|
||||
|
||||
vi.mocked(destinationService.findDestinationById).mockResolvedValue(
|
||||
mockDestination as any,
|
||||
);
|
||||
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
|
||||
mockApplication as any,
|
||||
);
|
||||
|
||||
const fullCommand = await restoreVolume(
|
||||
mockApplication.applicationId,
|
||||
mockDestination.destinationId,
|
||||
currentVolumeName,
|
||||
backupFileName,
|
||||
"",
|
||||
"application",
|
||||
);
|
||||
|
||||
console.log("📥 Executing REAL Dokploy restore for 1GB volume...");
|
||||
|
||||
// Execute the REAL command, skipping S3 download
|
||||
const restoreStartTime = Date.now();
|
||||
const commandWithoutS3 = fullCommand.replace(
|
||||
/rclone copyto[^\n]+/g,
|
||||
'echo "Skipping S3 download - file already present for test"',
|
||||
);
|
||||
|
||||
try {
|
||||
await execAsync(commandWithoutS3);
|
||||
const restoreTime = ((Date.now() - restoreStartTime) / 1000).toFixed(
|
||||
2,
|
||||
);
|
||||
console.log(`✅ 1GB restore command executed in ${restoreTime}s`);
|
||||
} catch (error: any) {
|
||||
console.error("Restore command failed:", error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Step 5: Verify data integrity on restored 1GB volume
|
||||
console.log("🔍 Verifying 1GB restored data...");
|
||||
|
||||
// Check marker file
|
||||
const { stdout: marker } = await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu cat /data/metadata/marker.txt
|
||||
`);
|
||||
expect(marker.trim()).toBe("test-marker-12345");
|
||||
|
||||
// Check info file
|
||||
const { stdout: info } = await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu cat /data/metadata/info.txt
|
||||
`);
|
||||
expect(info).toContain("Large restore test - Issue 3301");
|
||||
|
||||
// Verify large files exist
|
||||
const { stdout: fileList } = await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu ls -lh /data
|
||||
`);
|
||||
expect(fileList).toContain("large-file-1.dat");
|
||||
expect(fileList).toContain("large-file-2.dat");
|
||||
expect(fileList).toContain("large-file-3.dat");
|
||||
expect(fileList).toContain("large-file-4.dat");
|
||||
|
||||
// Verify total size
|
||||
const { stdout: sizeInfo } = await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu du -sh /data
|
||||
`);
|
||||
console.log(`Restored volume size: ${sizeInfo.trim()}`);
|
||||
|
||||
// Verify volume exists
|
||||
const { stdout: volumeCheck } = await execAsync(
|
||||
`docker volume ls --filter name=${currentVolumeName} --format "{{.Name}}"`,
|
||||
);
|
||||
expect(volumeCheck.trim()).toBe(currentVolumeName);
|
||||
|
||||
console.log("\n📊 Performance Summary:");
|
||||
console.log(` - Data creation: ${createTime}s`);
|
||||
console.log(` - Backup: ${backupTime}s`);
|
||||
console.log(
|
||||
"✅ 1GB restore test PASSED - Real Dokploy code handles large volumes correctly",
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
Reference in New Issue
Block a user