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
294 changes: 294 additions & 0 deletions apps/dokploy/__test__/env/environment-access-fallback.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
9 changes: 5 additions & 4 deletions apps/dokploy/components/dashboard/projects/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,17 +288,18 @@ export const ShowProjects = () => {
)
.some(Boolean);

const productionEnvironment = project?.environments.find(
(env) => env.isDefault,
);
// Find default environment from accessible environments, or fall back to first accessible environment
const accessibleEnvironment =
project?.environments.find((env) => env.isDefault) ||
project?.environments?.[0];

return (
<div
key={project.projectId}
className="w-full lg:max-w-md"
>
<Link
href={`/dashboard/project/${project.projectId}/environment/${productionEnvironment?.environmentId}`}
href={`/dashboard/project/${project.projectId}/environment/${accessibleEnvironment?.environmentId}`}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
{haveServicesWithDomains ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/dokploy/components/dashboard/search-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const SearchCommand = () => {
<CommandGroup heading={"Projects"}>
<CommandList>
{data?.map((project) => {
// Find default environment, or fall back to first environment
// Find default environment from accessible environments, or fall back to first accessible environment
const defaultEnvironment =
project.environments.find(
(environment) => environment.isDefault,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1621,9 +1621,39 @@ export async function getServerSideProps(
projectId: params.projectId,
});

await helpers.environment.one.fetch({
environmentId: params.environmentId,
});
// Try to fetch the requested environment
try {
await helpers.environment.one.fetch({
environmentId: params.environmentId,
});
} catch (error) {
// If user doesn't have access to requested environment, redirect to accessible one
const accessibleEnvironments =
await helpers.environment.byProjectId.fetch({
projectId: params.projectId,
});

if (accessibleEnvironments.length > 0) {
// Try to find default, otherwise use first accessible
const targetEnv =
accessibleEnvironments.find((env) => env.isDefault) ||
accessibleEnvironments[0];

return {
redirect: {
permanent: false,
destination: `/dashboard/project/${params.projectId}/environment/${targetEnv.environmentId}`,
},
};
}
// No accessible environments, redirect to home
return {
redirect: {
permanent: false,
destination: "/",
},
};
}

await helpers.environment.byProjectId.fetch({
projectId: params.projectId,
Expand Down