mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-24 20:06:55 +01:00
chore: unique project names validation on creation (#10970)
https://linear.app/unleash/issue/2-4024/we-should-validate-that-new-project-names-are-unique-ui-only Validates that new project names must be unique. Covers both: - Creating a new project - Editing an existing project --------- Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
ba25d7ada9
commit
3c392510f1
@ -18,6 +18,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { styled } from '@mui/material';
|
||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||
import type { ProjectOverviewSchema } from 'openapi';
|
||||
import useProjects from 'hooks/api/getters/useProjects/useProjects.ts';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
minHeight: 0,
|
||||
@ -72,7 +73,13 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
|
||||
);
|
||||
|
||||
const { editProject, loading } = useProjectApi();
|
||||
const { refetch } = useProjectOverview(id);
|
||||
const { refetch: refetchProjectOverview } = useProjectOverview(id);
|
||||
const { refetch: refetchProjects } = useProjects();
|
||||
|
||||
const refetch = () => {
|
||||
refetchProjectOverview();
|
||||
refetchProjects();
|
||||
};
|
||||
|
||||
const formatProjectApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
|
||||
@ -1,8 +1,28 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useProjectForm from './useProjectForm.js';
|
||||
import { test } from 'vitest';
|
||||
import { beforeEach, test, vi } from 'vitest';
|
||||
import { act } from 'react';
|
||||
|
||||
const mockUseProjects = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('hooks/api/getters/useProjects/useProjects.js', () => ({
|
||||
default: mockUseProjects,
|
||||
}));
|
||||
|
||||
const createProjectsResponse = (
|
||||
projects: Array<{ id: string; name: string }> = [],
|
||||
) => ({
|
||||
projects,
|
||||
error: undefined,
|
||||
loading: false,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseProjects.mockReset();
|
||||
mockUseProjects.mockReturnValue(createProjectsResponse());
|
||||
});
|
||||
|
||||
describe('configuring change requests', () => {
|
||||
test('setting project environments removes any change request envs that are not in the new project env list', () => {
|
||||
const { result } = renderHook(() => useProjectForm());
|
||||
@ -93,13 +113,100 @@ describe('payload generation', () => {
|
||||
});
|
||||
|
||||
describe('name validation', () => {
|
||||
const existingProjects = [
|
||||
{ id: 'project1', name: 'Project One' },
|
||||
{ id: 'project2', name: 'Project Two' },
|
||||
];
|
||||
|
||||
test.each([
|
||||
['An empty string', ''],
|
||||
['Just whitespace', ' '],
|
||||
])(`%s is not valid`, (_, value) => {
|
||||
const { result } = renderHook(() => useProjectForm());
|
||||
const { result } = renderHook(() => useProjectForm(undefined, 'valid'));
|
||||
|
||||
result.current.setProjectName(value);
|
||||
act(() => result.current.setProjectName(value));
|
||||
expect(result.current.validateName()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('accepts a unique project name on creation', () => {
|
||||
mockUseProjects.mockReturnValue(
|
||||
createProjectsResponse(existingProjects),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useProjectForm());
|
||||
|
||||
act(() => result.current.setProjectName('Brand New Project'));
|
||||
|
||||
expect(result.current.validateName()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('rejects duplicate project name on creation', () => {
|
||||
mockUseProjects.mockReturnValue(
|
||||
createProjectsResponse(existingProjects),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useProjectForm());
|
||||
|
||||
act(() => result.current.setProjectName('Project One'));
|
||||
|
||||
let isValid = true;
|
||||
act(() => {
|
||||
isValid = result.current.validateName();
|
||||
});
|
||||
|
||||
expect(isValid).toBeFalsy();
|
||||
expect(result.current.errors).toHaveProperty(
|
||||
'name',
|
||||
'This name is already taken by a different project.',
|
||||
);
|
||||
});
|
||||
|
||||
test('allows keeping the original name when editing, even if that name is also used by a different project', () => {
|
||||
mockUseProjects.mockReturnValue(
|
||||
createProjectsResponse(existingProjects),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useProjectForm('project3', 'Project One'),
|
||||
);
|
||||
|
||||
expect(result.current.validateName()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('rejects renaming to another existing project name', () => {
|
||||
mockUseProjects.mockReturnValue(
|
||||
createProjectsResponse(existingProjects),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useProjectForm('project1', 'Project One'),
|
||||
);
|
||||
|
||||
act(() => result.current.setProjectName('Project Two'));
|
||||
|
||||
let isValid = true;
|
||||
act(() => {
|
||||
isValid = result.current.validateName();
|
||||
});
|
||||
|
||||
expect(isValid).toBeFalsy();
|
||||
expect(result.current.errors).toHaveProperty(
|
||||
'name',
|
||||
'This name is already taken by a different project.',
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts renaming to a new unique project name', () => {
|
||||
mockUseProjects.mockReturnValue(
|
||||
createProjectsResponse(existingProjects),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useProjectForm('project1', 'Project One'),
|
||||
);
|
||||
|
||||
act(() => result.current.setProjectName('Project Three'));
|
||||
|
||||
expect(result.current.validateName()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import type { ProjectMode } from './useProjectEnterpriseSettingsForm.js';
|
||||
import useProjects from 'hooks/api/getters/useProjects/useProjects.js';
|
||||
|
||||
export const DEFAULT_PROJECT_STICKINESS = 'default';
|
||||
const useProjectForm = (
|
||||
@ -18,6 +19,8 @@ const useProjectForm = (
|
||||
{ requiredApprovals: number }
|
||||
> = {},
|
||||
) => {
|
||||
const { projects } = useProjects();
|
||||
|
||||
const { isEnterprise } = useUiConfig();
|
||||
const [projectId, setProjectId] = useState(initialProjectId);
|
||||
const [projectMode, setProjectMode] =
|
||||
@ -172,6 +175,17 @@ const useProjectForm = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
projectName !== initialProjectName &&
|
||||
projects.some(({ name }) => name === projectName)
|
||||
) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
name: 'This name is already taken by a different project.',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user