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
32 changes: 31 additions & 1 deletion apps/dokploy/__test__/deploy/github-webhook-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,34 @@ describe("GitHub app webhook auto-deploy", () => {
expect(res.json).toHaveBeenCalledWith({ message: "Deployed 1 apps" });
});

it("keeps remote application server id in queued push deployments", async () => {
mocks.applicationsFindMany.mockResolvedValue([
{
applicationId: "application-id",
serverId: "server-application",
watchPaths: null,
},
]);
const res = createResponse();

await handler(createPushRequest("main"), res);

expect(mocks.queueAdd).toHaveBeenCalledWith(
"deployments",
expect.objectContaining({
applicationId: "application-id",
applicationType: "application",
server: true,
serverId: "server-application",
type: "deploy",
}),
expect.objectContaining({
removeOnComplete: true,
removeOnFail: true,
}),
);
});

it("matches compose push events using repository owner login fallback", async () => {
mocks.applicationsFindMany.mockResolvedValue([]);
mocks.composeFindMany.mockImplementation(({ where }) => {
Expand All @@ -239,7 +267,7 @@ describe("GitHub app webhook auto-deploy", () => {
? [
{
composeId: "compose-id",
serverId: null,
serverId: "server-compose",
watchPaths: null,
},
]
Expand All @@ -255,6 +283,8 @@ describe("GitHub app webhook auto-deploy", () => {
expect.objectContaining({
applicationType: "compose",
composeId: "compose-id",
server: true,
serverId: "server-compose",
type: "deploy",
}),
expect.objectContaining({
Expand Down
152 changes: 152 additions & 0 deletions apps/dokploy/__test__/env/application-env-upsert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { upsertApplicationEnvironment } from "@dokploy/server/services/application";
import { getApplicationEnvRevision } from "@dokploy/server/utils/env-upsert";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildApplicationEnvUpsertDeploymentJob } from "@/server/api/utils/application-env-upsert";

const dbMocks = vi.hoisted(() => {
const returning = vi.fn();
const where = vi.fn(() => ({ returning }));
const set = vi.fn(() => ({ where }));
const update = vi.fn(() => ({ set }));
const findFirst = vi.fn();

return {
findFirst,
returning,
set,
update,
where,
};
});

vi.mock("@dokploy/server/db", () => ({
db: {
query: {
applications: {
findFirst: dbMocks.findFirst,
},
},
update: dbMocks.update,
},
}));

const mockApplication = (env: string | null = null) => ({
applicationId: "app_1",
env,
});

describe("upsertApplicationEnvironment", () => {
beforeEach(() => {
vi.clearAllMocks();
dbMocks.returning.mockResolvedValue([mockApplication()]);
});

it("returns dry run metadata without saving raw values", async () => {
dbMocks.findFirst.mockResolvedValue(
mockApplication("API_URL=https://old.example.com\nREDIS_PASSWORD=old"),
);

const result = await upsertApplicationEnvironment({
applicationId: "app_1",
variables: {
API_URL: "https://api.example.com",
REDIS_PASSWORD: "new",
},
dryRun: true,
});

expect(dbMocks.update).not.toHaveBeenCalled();
expect(result).toEqual({
applicationId: "app_1",
changed: true,
revision: getApplicationEnvRevision(
"app_1",
"API_URL=https://old.example.com\nREDIS_PASSWORD=old",
),
dryRun: true,
variables: [
{
name: "API_URL",
action: "updated",
secret: false,
},
{
name: "REDIS_PASSWORD",
action: "updated",
secret: true,
},
],
});
expect(JSON.stringify(result)).not.toContain("new");
});

it("saves only the merged environment when the revision matches", async () => {
const currentEnv = "API_URL=https://old.example.com\nREDIS_PASSWORD=old";
dbMocks.findFirst.mockResolvedValue(mockApplication(currentEnv));

const result = await upsertApplicationEnvironment({
applicationId: "app_1",
variables: {
API_URL: "https://api.example.com",
REDIS_HOST: "redis-dev",
},
expectedRevision: getApplicationEnvRevision("app_1", currentEnv),
});

expect(dbMocks.set).toHaveBeenCalledWith({
env: "API_URL=https://api.example.com\nREDIS_PASSWORD=old\nREDIS_HOST=redis-dev",
});
expect(result.changed).toBe(true);
expect(result.revision).toBe(
getApplicationEnvRevision(
"app_1",
"API_URL=https://api.example.com\nREDIS_PASSWORD=old\nREDIS_HOST=redis-dev",
),
);
});

it("rejects stale expected revisions without writing", async () => {
dbMocks.findFirst.mockResolvedValue(mockApplication("API_URL=https://old"));

await expect(
upsertApplicationEnvironment({
applicationId: "app_1",
variables: {
API_URL: "https://api.example.com",
},
expectedRevision: "env:stale",
}),
).rejects.toMatchObject({
code: "CONFLICT",
message: "Application environment revision does not match",
});
expect(dbMocks.update).not.toHaveBeenCalled();
});
});

describe("buildApplicationEnvUpsertDeploymentJob", () => {
it("keeps the remote server id for queue partitioning", () => {
expect(
buildApplicationEnvUpsertDeploymentJob({
applicationId: "app_1",
serverId: "server_1",
}),
).toMatchObject({
applicationId: "app_1",
applicationType: "application",
type: "redeploy",
server: true,
serverId: "server_1",
});
});

it("keeps local redeploy jobs in the local queue partition", () => {
const jobData = buildApplicationEnvUpsertDeploymentJob({
applicationId: "app_1",
serverId: null,
});

expect(jobData.server).toBe(false);
expect(jobData.serverId).toBeUndefined();
});
});
86 changes: 86 additions & 0 deletions apps/dokploy/__test__/env/upsert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
getApplicationEnvRevision,
isSecretEnvName,
upsertEnvVariables,
} from "@dokploy/server/utils/env-upsert";
import { describe, expect, it } from "vitest";

describe("upsertEnvVariables", () => {
it("updates existing variables and appends new variables without removing existing secrets", () => {
const result = upsertEnvVariables(
`# Existing app env
API_URL=https://old.example.com
REDIS_PASSWORD=secret-value
`,
{
API_URL: "https://api.example.com",
REDIS_HOST: "redis-dev",
REDIS_PASSWORD: "secret-value",
},
);

expect(result.changed).toBe(true);
expect(result.env).toBe(`# Existing app env
API_URL=https://api.example.com
REDIS_PASSWORD=secret-value
REDIS_HOST=redis-dev
`);
expect(result.variables).toEqual([
{
name: "API_URL",
action: "updated",
secret: false,
},
{
name: "REDIS_HOST",
action: "created",
secret: false,
},
{
name: "REDIS_PASSWORD",
action: "unchanged",
secret: true,
},
]);
});

it("quotes values that would be unsafe as plain dotenv values", () => {
const result = upsertEnvVariables("", {
PLAIN_VALUE: "redis-dev",
SPACED_VALUE: "redis dev",
MULTILINE_VALUE: "first\nsecond",
HASH_VALUE: "value # not a comment",
});

expect(result.env).toBe(`PLAIN_VALUE=redis-dev
SPACED_VALUE="redis dev"
MULTILINE_VALUE="first\\nsecond"
HASH_VALUE="value # not a comment"`);
});

it("marks common credential names as secret metadata", () => {
expect(isSecretEnvName("REDIS_PASSWORD")).toBe(true);
expect(isSecretEnvName("API_TOKEN")).toBe(true);
expect(isSecretEnvName("PUBLIC_URL")).toBe(false);
});

it("creates a stable opaque revision that changes when the env changes", () => {
const firstRevision = getApplicationEnvRevision(
"app_1",
"REDIS_PASSWORD=secret-value",
);
const sameRevision = getApplicationEnvRevision(
"app_1",
"REDIS_PASSWORD=secret-value",
);
const nextRevision = getApplicationEnvRevision(
"app_1",
"REDIS_PASSWORD=new-secret-value",
);

expect(firstRevision).toBe(sameRevision);
expect(firstRevision).not.toBe(nextRevision);
expect(firstRevision).toMatch(/^env:/);
expect(firstRevision).not.toContain("secret-value");
});
});
43 changes: 42 additions & 1 deletion apps/dokploy/__test__/git-provider/git-provider-access.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
canEditDeployGitSource,
getAccessibleGitProviderIds,
redactGitProviderSecrets,
} from "@dokploy/server/services/git-provider";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockDb = vi.hoisted(() => ({
query: {
Expand Down Expand Up @@ -67,6 +68,46 @@ beforeEach(() => {
mockHasValidLicense.mockResolvedValue(false);
});

describe("redactGitProviderSecrets", () => {
it("removes nested provider credential fields", () => {
const redacted = redactGitProviderSecrets({
name: "compose",
github: {
githubClientId: "client-id",
githubClientSecret: "client-secret",
githubPrivateKey: "private-key",
githubWebhookSecret: "webhook-secret",
},
gitlab: {
gitlabUrl: "https://gitlab.example.com",
secret: "secret",
accessToken: "access-token",
refreshToken: "refresh-token",
},
gitea: {
giteaUrl: "https://gitea.example.com",
clientSecret: "client-secret",
accessToken: "access-token",
refreshToken: "refresh-token",
},
bitbucket: {
bitbucketWorkspaceName: "workspace",
appPassword: "app-password",
apiToken: "api-token",
},
});

expect(redacted.github).toEqual({ githubClientId: "client-id" });
expect(redacted.gitlab).toEqual({
gitlabUrl: "https://gitlab.example.com",
});
expect(redacted.gitea).toEqual({ giteaUrl: "https://gitea.example.com" });
expect(redacted.bitbucket).toEqual({
bitbucketWorkspaceName: "workspace",
});
});
});

describe("getAccessibleGitProviderIds", () => {
describe("owner", () => {
beforeEach(() => {
Expand Down
Loading
Loading