This commit is contained in:
user
2026-02-10 23:43:34 +03:00
parent 744ebab15a
commit ce9ba60902
21 changed files with 24797 additions and 20005 deletions
@@ -1,3 +1,4 @@
import { existsSync } from "node:fs";
import path from "node:path";
import type { ApplicationNested } from "@dokploy/server";
@@ -8,6 +9,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const REAL_TEST_TIMEOUT = 180000; // 3 minutes
// Mock constants to avoid load error
vi.mock("@dokploy/server/constants", () => ({
paths: () => ({
LOGS_PATH: "/tmp/dokploy-test-real/logs",
APPLICATIONS_PATH: "/tmp/dokploy-test-real/applications",
PATCH_REPOS_PATH: "/tmp/dokploy-test-real/patch-repos",
}),
IS_CLOUD: false,
docker: {},
}));
// Mock ONLY database and notifications
vi.mock("@dokploy/server/db", () => {
const createChainableMock = (): any => {
@@ -67,6 +79,16 @@ vi.mock("@dokploy/server/services/rollbacks", () => ({
createRollback: vi.fn(),
}));
vi.mock("@dokploy/server/services/patch", async (importOriginal) => {
const actual = await importOriginal<
typeof import("@dokploy/server/services/patch")
>();
return {
...actual,
findPatchesByApplicationId: vi.fn().mockResolvedValue([]),
};
});
// NOT mocked (executed for real):
// - execAsync
// - cloneGitRepository
@@ -78,6 +100,11 @@ import * as adminService from "@dokploy/server/services/admin";
import * as applicationService from "@dokploy/server/services/application";
import { deployApplication } from "@dokploy/server/services/application";
import * as deploymentService from "@dokploy/server/services/deployment";
import * as patchService from "@dokploy/server/services/patch";
import { generatePatch } from "@dokploy/server/services/patch";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const createMockApplication = (
overrides: Partial<ApplicationNested> = {},
@@ -474,6 +501,105 @@ describe(
},
REAL_TEST_TIMEOUT,
);
it(
"should REALLY apply patches from database during deployment",
async () => {
// 1. Setup local temporary git repo
const tempRepo = await mkdtemp(join(tmpdir(), "real-patch-repo-"));
// Helper for local git commands
const execLocal = async (cmd: string) => execAsync(cmd, { cwd: tempRepo });
await execLocal("git init");
await execLocal("git config user.email '[email protected]'");
await execLocal("git config user.name 'Dokploy Test'");
// Create a simple Dockerfile and server script
// We use a simple python server to verify output
await writeFile(join(tempRepo, "app.py"), "print('Original App')\n");
await writeFile(
join(tempRepo, "Dockerfile"),
"FROM python:3.9-slim\nCOPY app.py .\nCMD [\"python\", \"app.py\"]\n",
);
await execLocal("git add .");
await execLocal("git commit -m 'Initial commit'");
// Ensure master/main branch exists (git init might create master or main depending on config)
// We force create a branch named 'main' to be consistent
await execLocal("git checkout -b main || git checkout main");
// 2. Mock Application to use this local repo
const patchAppName = `real-patch-app-${Date.now()}`;
const patchApp = createMockApplication({
appName: patchAppName,
buildType: "dockerfile",
customGitUrl: `file://${tempRepo}`,
customGitBranch: "main",
dockerfile: "Dockerfile",
});
currentAppName = patchAppName;
allTestAppNames.push(patchAppName);
// Setup standard mocks
vi.mocked(db.query.applications.findFirst).mockResolvedValue(
patchApp as any,
);
vi.mocked(applicationService.findApplicationById).mockResolvedValue(
patchApp as any,
);
// 3. Generate a patch
// We modify the file, generate patch, and then reset.
const newContent = "print('Patched App')\n";
const patchContent = await generatePatch({
codePath: tempRepo,
filePath: "app.py",
newContent,
serverId: null,
});
// 4. Mock patch service to return this patch
vi.mocked(patchService.findPatchesByApplicationId).mockResolvedValue([
{
patchId: "test-patch-1",
applicationId: "test-app-id",
composeId: null,
filePath: "app.py",
content: patchContent,
enabled: true,
createdAt: new Date().toISOString(),
} as any,
]);
console.log(`\n🚀 Testing deployment with patch: ${currentAppName}`);
// 5. Deploy
const result = await deployApplication({
applicationId: "test-app-id",
titleLog: "Real Patch Test",
descriptionLog: "Testing patch application",
});
expect(result).toBe(true);
// 6. Verify Log contains "Applying patch"
const { stdout: logContent } = await execAsync(
`cat ${currentDeployment.logPath}`,
);
// The implementation logs "Applying patch: ..."
expect(logContent).toContain("Applying patch");
expect(logContent).toContain("app.py");
console.log("✅ Verified patch execution logs");
// 7. Verify the deployed image contains the patched code
// We run the image and check output
const { stdout: runOutput } = await execAsync(
`docker run --rm ${patchAppName}`,
);
expect(runOutput.trim()).toBe("Patched App");
console.log("✅ Verified patched output:", runOutput.trim());
},
REAL_TEST_TIMEOUT,
);
},
REAL_TEST_TIMEOUT,
);
@@ -0,0 +1,106 @@
import { generatePatch } from "@dokploy/server/services/patch";
import { describe, expect, it, afterEach } from "vitest";
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsyncLocal = promisify(exec);
describe("Patch System Integration", () => {
let tempDir: string;
afterEach(async () => {
if (tempDir) {
await rm(tempDir, { recursive: true, force: true });
}
});
it("should generate a patch that can be successfully applied via git", async () => {
// Setup repo
tempDir = await mkdtemp(join(tmpdir(), "dokploy-patch-test-"));
const fileName = "test.txt";
const filePath = join(tempDir, fileName);
await execAsyncLocal("git init", { cwd: tempDir });
await execAsyncLocal("git config user.email '[email protected]'", { cwd: tempDir });
await execAsyncLocal("git config user.name 'Test'", { cwd: tempDir });
// Original content
await writeFile(filePath, "line1\nline2\n");
await execAsyncLocal(`git add ${fileName}`, { cwd: tempDir });
await execAsyncLocal("git commit -m 'init'", { cwd: tempDir });
// Generate patch (modify content)
const newContent = "line1\nline2\nline3\n";
const patchContent = await generatePatch({
codePath: tempDir,
filePath: fileName,
newContent,
serverId: null,
});
// Verify patch format
expect(patchContent.endsWith("\n")).toBe(true);
// Reset file (generatePatch does reset, but ensure it)
await execAsyncLocal("git checkout .", { cwd: tempDir });
const savedContent = await readFile(filePath, "utf-8");
expect(savedContent).toBe("line1\nline2\n");
// Apply patch verification
// We simulate what Deployment Service does: write patch to file and run git apply
const patchFile = join(tempDir, "changes.patch");
await writeFile(patchFile, patchContent);
try {
await execAsyncLocal(`git apply --whitespace=fix ${patchFile}`, { cwd: tempDir });
} catch (e: any) {
console.error("Git apply failed:", e.message);
console.log("Patch content:", JSON.stringify(patchContent));
throw e;
}
const appliedContent = await readFile(filePath, "utf-8");
expect(appliedContent).toBe(newContent);
});
it("should handle files created without trailing newline", async () => {
// Setup repo
tempDir = await mkdtemp(join(tmpdir(), "dokploy-patch-test-noline-"));
const fileName = "noline.txt";
const filePath = join(tempDir, fileName);
await execAsyncLocal("git init", { cwd: tempDir });
await execAsyncLocal("git config user.email '[email protected]'", { cwd: tempDir });
await execAsyncLocal("git config user.name 'Test'", { cwd: tempDir });
// Original content WITHOUT newline
await writeFile(filePath, "line1");
await execAsyncLocal(`git add ${fileName}`, { cwd: tempDir });
await execAsyncLocal("git commit -m 'init'", { cwd: tempDir });
// Generate patch
const newContent = "line1\nline2";
const patchContent = await generatePatch({
codePath: tempDir,
filePath: fileName,
newContent,
serverId: null,
});
// Verify patch format
expect(patchContent.endsWith("\n")).toBe(true);
// Apply patch
const patchFile = join(tempDir, "changes.patch");
await writeFile(patchFile, patchContent);
await execAsyncLocal(`git apply --whitespace=fix ${patchFile}`, { cwd: tempDir });
const appliedContent = await readFile(filePath, "utf-8");
expect(appliedContent).toBe(newContent);
});
});