Merge branch 'canary' into ulimits-at-0a401843

This commit is contained in:
Mauricio Siu
2026-02-08 23:30:14 -06:00
234 changed files with 69952 additions and 5923 deletions

View File

@@ -4,21 +4,30 @@ import { describe, expect, it } from "vitest";
describe("addDokployNetworkToService", () => {
it("should add network to an empty array", () => {
const result = addDokployNetworkToService([]);
expect(result).toEqual(["dokploy-network"]);
expect(result).toEqual(["dokploy-network", "default"]);
});
it("should not add duplicate network to an array", () => {
const result = addDokployNetworkToService(["dokploy-network"]);
expect(result).toEqual(["dokploy-network"]);
expect(result).toEqual(["dokploy-network", "default"]);
});
it("should add network to an existing array with other networks", () => {
const result = addDokployNetworkToService(["other-network"]);
expect(result).toEqual(["other-network", "dokploy-network"]);
expect(result).toEqual(["other-network", "dokploy-network", "default"]);
});
it("should add network to an object if networks is an object", () => {
const result = addDokployNetworkToService({ "other-network": {} });
expect(result).toEqual({ "other-network": {}, "dokploy-network": {} });
expect(result).toEqual({
"other-network": {},
"dokploy-network": {},
default: {},
});
});
it("should not duplicate default network when already present", () => {
const result = addDokployNetworkToService(["default", "dokploy-network"]);
expect(result).toEqual(["default", "dokploy-network"]);
});
});

View File

@@ -25,10 +25,11 @@ if (typeof window === "undefined") {
}
const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
railpackVersion: "0.15.4",
applicationId: "",
previewLabels: [],
createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "",
giteaBranch: "",
buildServerId: "",

View File

@@ -0,0 +1,294 @@
import { describe, expect, it } from "vitest";
// Type definitions matching the project structure
type Environment = {
environmentId: string;
name: string;
isDefault: boolean;
};
type Project = {
projectId: string;
name: string;
environments: Environment[];
};
/**
* Helper function that selects the appropriate environment for a user
* This matches the logic used in search-command.tsx and show.tsx
*/
function selectAccessibleEnvironment(
project: Project | null | undefined,
): Environment | null {
if (!project || !project.environments || project.environments.length === 0) {
return null;
}
// Find default environment from accessible environments, or fall back to first accessible environment
const defaultEnvironment =
project.environments.find((environment) => environment.isDefault) ||
project.environments[0];
return defaultEnvironment || null;
}
describe("Environment Access Fallback", () => {
describe("selectAccessibleEnvironment", () => {
it("should return default environment when user has access to it", () => {
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
{
environmentId: "env-prod",
name: "production",
isDefault: true,
},
{
environmentId: "env-dev",
name: "development",
isDefault: false,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-prod");
expect(result?.isDefault).toBe(true);
});
it("should return first accessible environment when user doesn't have access to default", () => {
// Simulating filtered environments (user only has access to development)
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
// Note: production is not in the list because user doesn't have access
{
environmentId: "env-dev",
name: "development",
isDefault: false,
},
{
environmentId: "env-staging",
name: "staging",
isDefault: false,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-dev");
expect(result?.name).toBe("development");
});
it("should return first environment when no default is marked but environments exist", () => {
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
{
environmentId: "env-dev",
name: "development",
isDefault: false,
},
{
environmentId: "env-staging",
name: "staging",
isDefault: false,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-dev");
});
it("should return null when project has no accessible environments", () => {
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [],
};
const result = selectAccessibleEnvironment(project);
expect(result).toBeNull();
});
it("should return null when project is null", () => {
const result = selectAccessibleEnvironment(null);
expect(result).toBeNull();
});
it("should return null when project is undefined", () => {
const result = selectAccessibleEnvironment(undefined);
expect(result).toBeNull();
});
it("should handle project with single accessible environment", () => {
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
{
environmentId: "env-dev",
name: "development",
isDefault: false,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-dev");
});
it("should prioritize default environment even when it's not first in the array", () => {
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
{
environmentId: "env-dev",
name: "development",
isDefault: false,
},
{
environmentId: "env-staging",
name: "staging",
isDefault: false,
},
{
environmentId: "env-prod",
name: "production",
isDefault: true,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-prod");
expect(result?.isDefault).toBe(true);
});
it("should handle multiple default environments by returning the first one found", () => {
// Edge case: multiple environments marked as default (shouldn't happen, but test it)
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
{
environmentId: "env-prod-1",
name: "production-1",
isDefault: true,
},
{
environmentId: "env-prod-2",
name: "production-2",
isDefault: true,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.isDefault).toBe(true);
// Should return the first default found
expect(result?.environmentId).toBe("env-prod-1");
});
it("should work correctly when user has access to multiple environments including default", () => {
const project: Project = {
projectId: "proj-1",
name: "Test Project",
environments: [
{
environmentId: "env-prod",
name: "production",
isDefault: true,
},
{
environmentId: "env-dev",
name: "development",
isDefault: false,
},
{
environmentId: "env-staging",
name: "staging",
isDefault: false,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-prod");
expect(result?.isDefault).toBe(true);
});
it("should handle real-world scenario: user with only development access", () => {
// This simulates the exact bug we're fixing:
// User has access to development but not production (default)
// The filtered environments array only contains development
const project: Project = {
projectId: "proj-1",
name: "My Project",
environments: [
// Only development is accessible (production was filtered out)
{
environmentId: "env-dev-123",
name: "development",
isDefault: false,
},
],
};
const result = selectAccessibleEnvironment(project);
expect(result).not.toBeNull();
expect(result?.environmentId).toBe("env-dev-123");
expect(result?.name).toBe("development");
// Should not be null even though it's not the default
});
});
describe("Environment selection edge cases", () => {
it("should handle project with environments property as undefined", () => {
const project = {
projectId: "proj-1",
name: "Test Project",
environments: undefined,
} as unknown as Project;
const result = selectAccessibleEnvironment(project);
expect(result).toBeNull();
});
it("should handle project with null environments array", () => {
const project = {
projectId: "proj-1",
name: "Test Project",
environments: null,
} as unknown as Project;
const result = selectAccessibleEnvironment(project);
expect(result).toBeNull();
});
});
});

View File

@@ -0,0 +1,184 @@
import { getEnviromentVariablesObject } from "@dokploy/server/index";
import { describe, expect, it } from "vitest";
const projectEnv = `
ENVIRONMENT=staging
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
PORT=3000
`;
const environmentEnv = `
NODE_ENV=development
API_URL=https://api.dev.example.com
REDIS_URL=redis://localhost:6379
DATABASE_NAME=dev_database
SECRET_KEY=env-secret-123
`;
describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
it("resolves environment variables correctly for Stack compose", () => {
const serviceEnv = `
FOO=\${{environment.NODE_ENV}}
BAR=\${{environment.API_URL}}
BAZ=test
`;
const result = getEnviromentVariablesObject(
serviceEnv,
projectEnv,
environmentEnv,
);
expect(result).toEqual({
FOO: "development",
BAR: "https://api.dev.example.com",
BAZ: "test",
});
});
it("resolves both project and environment variables for Stack compose", () => {
const serviceEnv = `
ENVIRONMENT=\${{project.ENVIRONMENT}}
NODE_ENV=\${{environment.NODE_ENV}}
API_URL=\${{environment.API_URL}}
DATABASE_URL=\${{project.DATABASE_URL}}
SERVICE_PORT=4000
`;
const result = getEnviromentVariablesObject(
serviceEnv,
projectEnv,
environmentEnv,
);
expect(result).toEqual({
ENVIRONMENT: "staging",
NODE_ENV: "development",
API_URL: "https://api.dev.example.com",
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db",
SERVICE_PORT: "4000",
});
});
it("handles multiple environment references in single value for Stack compose", () => {
const multiRefEnv = `
HOST=localhost
PORT=5432
USERNAME=postgres
PASSWORD=secret123
`;
const serviceEnv = `
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
`;
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
expect(result).toEqual({
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
});
});
it("throws error for undefined environment variables in Stack compose", () => {
const serviceWithUndefined = `
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
`;
expect(() =>
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
});
it("allows service variables to override environment variables in Stack compose", () => {
const serviceOverrideEnv = `
NODE_ENV=production
API_URL=\${{environment.API_URL}}
`;
const result = getEnviromentVariablesObject(
serviceOverrideEnv,
"",
environmentEnv,
);
expect(result).toEqual({
NODE_ENV: "production",
API_URL: "https://api.dev.example.com",
});
});
it("resolves complex references with project, environment, and service variables for Stack compose", () => {
const complexServiceEnv = `
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
SERVICE_NAME=my-service
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
`;
const result = getEnviromentVariablesObject(
complexServiceEnv,
projectEnv,
environmentEnv,
);
expect(result).toEqual({
FULL_DATABASE_URL:
"postgres://postgres:postgres@localhost:5432/project_db/dev_database",
API_ENDPOINT: "https://api.dev.example.com/staging/api",
SERVICE_NAME: "my-service",
COMPLEX_VAR: "my-service-development-staging",
});
});
it("maintains precedence: service > environment > project in Stack compose", () => {
const conflictingProjectEnv = `
NODE_ENV=production-project
API_URL=https://project.api.com
DATABASE_NAME=project_db
`;
const conflictingEnvironmentEnv = `
NODE_ENV=development-environment
API_URL=https://environment.api.com
DATABASE_NAME=env_db
`;
const serviceWithConflicts = `
NODE_ENV=service-override
PROJECT_ENV=\${{project.NODE_ENV}}
ENV_VAR=\${{environment.API_URL}}
DB_NAME=\${{environment.DATABASE_NAME}}
`;
const result = getEnviromentVariablesObject(
serviceWithConflicts,
conflictingProjectEnv,
conflictingEnvironmentEnv,
);
expect(result).toEqual({
NODE_ENV: "service-override",
PROJECT_ENV: "production-project",
ENV_VAR: "https://environment.api.com",
DB_NAME: "env_db",
});
});
it("handles empty environment variables in Stack compose", () => {
const serviceWithEmpty = `
SERVICE_VAR=test
PROJECT_VAR=\${{project.ENVIRONMENT}}
`;
const result = getEnviromentVariablesObject(
serviceWithEmpty,
projectEnv,
"",
);
expect(result).toEqual({
SERVICE_VAR: "test",
PROJECT_VAR: "staging",
});
});
});

View File

@@ -14,11 +14,11 @@ type MockCreateServiceOptions = {
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
vi.hoisted(() => {
const inspect = vi.fn<[], Promise<never>>();
const inspect = vi.fn<() => Promise<never>>();
const getService = vi.fn(() => ({ inspect }));
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
async () => undefined,
);
const createService = vi.fn<
(opts: MockCreateServiceOptions) => Promise<void>
>(async () => undefined);
const getRemoteDocker = vi.fn(async () => ({
getService,
createService,
@@ -82,7 +82,9 @@ describe("mechanizeDockerContainer", () => {
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
const call = createServiceMock.mock.calls[0] as
| [MockCreateServiceOptions]
| undefined;
if (!call) {
throw new Error("createServiceMock should have been called once");
}
@@ -99,7 +101,9 @@ describe("mechanizeDockerContainer", () => {
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
const call = createServiceMock.mock.calls[0] as
| [MockCreateServiceOptions]
| undefined;
if (!call) {
throw new Error("createServiceMock should have been called once");
}

View File

@@ -0,0 +1,40 @@
import { vi } from "vitest";
/**
* Mock the DB module so tests that import from @dokploy/server (barrel)
* never open a real TCP connection to PostgreSQL (e.g. in CI where no DB runs).
* Without this, loading the server barrel pulls in lib/auth and db, which
* connect to localhost:5432 and cause ECONNREFUSED.
*/
vi.mock("@dokploy/server/db", () => {
const chain = () => chain;
chain.set = () => chain;
chain.where = () => chain;
chain.values = () => chain;
chain.returning = () => Promise.resolve([{}]);
chain.then = undefined;
const tableMock = {
findFirst: vi.fn(() => Promise.resolve(undefined)),
findMany: vi.fn(() => Promise.resolve([])),
insert: vi.fn(() => Promise.resolve([{}])),
update: vi.fn(() => chain),
delete: vi.fn(() => chain),
};
const createQueryMock = () => tableMock;
return {
db: {
select: vi.fn(() => chain),
insert: vi.fn(() => ({
values: () => ({ returning: () => Promise.resolve([{}]) }),
})),
update: vi.fn(() => chain),
delete: vi.fn(() => chain),
query: new Proxy({} as Record<string, typeof tableMock>, {
get: () => tableMock,
}),
},
dbUrl: "postgres://mock:mock@localhost:5432/mock",
};
});

View File

@@ -3,11 +3,12 @@ import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
railpackVersion: "0.15.4",
rollbackActive: false,
applicationId: "",
previewLabels: [],
createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "",
giteaRepository: "",
giteaOwner: "",

View File

@@ -7,10 +7,15 @@ export default defineConfig({
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
pool: "forks",
setupFiles: [path.resolve(__dirname, "setup.ts")],
},
define: {
"process.env": {
NODE: "test",
GITHUB_CLIENT_ID: "test",
GITHUB_CLIENT_SECRET: "test",
GOOGLE_CLIENT_ID: "test",
GOOGLE_CLIENT_SECRET: "test",
},
},
plugins: [