Merge pull request #4690 from Infisical/fix/error-on-duplicate-folder-creation

fix: returns error on duplicate folder creation
This commit is contained in:
Piyush Gupta
2025-10-20 08:49:43 +05:30
committed by GitHub
7 changed files with 144 additions and 34 deletions

View File

@@ -40,7 +40,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
{ name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name });
// check for default environments
@@ -57,7 +57,7 @@ describe("Secret Folder Router", async () => {
{
path: "/",
expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3
}
},
@@ -162,4 +162,25 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0);
});
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
});

View File

@@ -41,7 +41,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
{ name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name });
// check for default environments
@@ -58,7 +58,7 @@ describe("Secret Folder Router", async () => {
{
path: "/",
expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3
}
},
@@ -163,4 +163,26 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0);
});
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v2/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
projectId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
});

View File

@@ -118,24 +118,11 @@ export const secretFolderServiceFactory = ({
});
}
// check if the exact folder already exists
const existingFolder = await folderDAL.findOne(
{
envId: env.id,
parentId: parentFolder.id,
name,
isReserved: false
},
tx
);
if (existingFolder) {
return existingFolder;
}
// exact folder case
if (parentFolder.path === pathWithFolder) {
return parentFolder;
throw new BadRequestError({
message: `Folder with name '${name}' already exists in path '${secretPath}'`
});
}
let currentParentId = parentFolder.id;

View File

@@ -2,6 +2,7 @@ export {
useCreateFolder,
useDeleteFolder,
useGetFoldersByEnv,
useGetOrCreateFolder,
useGetProjectFolders,
useUpdateFolder
} from "./queries";

View File

@@ -140,6 +140,59 @@ export const useGetFoldersByEnv = ({
return { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv };
};
export const useGetOrCreateFolder = () => {
const queryClient = useQueryClient();
return useMutation<TSecretFolder, object, TCreateFolderDTO>({
mutationFn: async (dto) => {
const { data: existingFolder } = await apiRequest.get<{ folders: TSecretFolder[] }>(
"/api/v2/folders",
{
params: {
projectId: dto.projectId,
environment: dto.environment,
path: dto.path || "/"
}
}
);
const folder = existingFolder.folders.find((f) => f.name === dto.name);
if (folder) return folder;
const { data } = await apiRequest.post("/api/v2/folders", {
...dto,
projectId: dto.projectId
});
return data;
},
onSuccess: (_, { projectId, environment, path }) => {
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({
projectId,
secretPath: path ?? "/"
})
});
queryClient.invalidateQueries({
queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ projectId, environment, directory: path })
});
}
});
};
export const useCreateFolder = () => {
const queryClient = useQueryClient();
@@ -170,6 +223,9 @@ export const useCreateFolder = () => {
queryClient.invalidateQueries({
queryKey: commitKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ projectId, environment, directory: path })
});
}
});
};

View File

@@ -86,6 +86,7 @@ import {
useCreateSecretV3,
useDeleteSecretV3,
useGetImportedSecretsAllEnvs,
useGetOrCreateFolder,
useGetWsTags,
useUpdateSecretV3
} from "@app/hooks/api";
@@ -367,6 +368,7 @@ export const OverviewPage = () => {
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: getOrCreateFolder } = useGetOrCreateFolder();
const { mutateAsync: updateFolderBatch } = useUpdateFolderBatch();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
@@ -384,16 +386,32 @@ export const OverviewPage = () => {
] as const);
const handleFolderCreate = async (folderName: string, description: string | null) => {
const promises = userAvailableEnvs.map((env) => {
const environment = env.slug;
return createFolder({
name: folderName,
path: secretPath,
environment,
projectId,
description
const promises = userAvailableEnvs
.map((env) => {
const environment = env.slug;
const isFolderPresent = isFolderPresentInEnv(folderName, environment);
if (isFolderPresent) {
return undefined;
}
return createFolder({
name: folderName,
path: secretPath,
environment,
projectId,
description
});
})
.filter((promise) => promise !== undefined);
if (promises.length === 0) {
handlePopUpClose("addFolder");
createNotification({
type: "info",
text: "Folder already exists in all environments"
});
});
return;
}
const results = await Promise.allSettled(promises);
const isFoldersAdded = results.some((result) => result.status === "fulfilled");
@@ -481,7 +499,7 @@ export const OverviewPage = () => {
})
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
await getOrCreateFolder({
projectId,
path: parentPath,
environment: env,
@@ -645,7 +663,7 @@ export const OverviewPage = () => {
})
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
await getOrCreateFolder({
projectId,
environment: slug,
path: parentPath,

View File

@@ -25,7 +25,12 @@ import {
} from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
import {
useCreateSecretV3,
useCreateWsTag,
useGetOrCreateFolder,
useGetWsTags
} from "@app/hooks/api";
import { SecretType } from "@app/hooks/api/types";
const typeSchema = z
@@ -64,7 +69,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
const environments = currentProject?.environments || [];
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: getOrCreateFolder } = useGetOrCreateFolder();
const { data: projectTags, isPending: isTagsLoading } = useGetWsTags(
canReadTags ? projectId : ""
);
@@ -92,7 +97,7 @@ export const CreateSecretForm = ({ secretPath = "/", onClose }: Props) => {
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
await getOrCreateFolder({
projectId,
path: parentPath,
environment,