diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index 0fd25fce6..6e49c8991 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -41,6 +41,7 @@ import { useRegistrationValidation, useHostnameValidation, useKernelValidation, + useUsersValidation, } from './utilities/useValidation'; import { isAwsAccountIdValid, @@ -230,6 +231,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const firstBootValidation = useFirstBootValidation(); // Details const detailsValidation = useDetailsValidation(); + // Users + const usersValidation = useUsersValidation(); let startIndex = 1; // default index if (isEdit) { @@ -462,7 +465,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { key="wizard-users" isHidden={!isUsersEnabled} footer={ - + } > diff --git a/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx b/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx index 48b433cdc..ad633f724 100644 --- a/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx +++ b/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx @@ -13,6 +13,7 @@ import { setUserPasswordByIndex, setUserSshKeyByIndex, } from '../../../../../store/wizardSlice'; +import { useUsersValidation } from '../../../utilities/useValidation'; import { HookValidatedInput } from '../../../ValidatedTextInput'; const UserInfo = () => { const dispatch = useAppDispatch(); @@ -45,10 +46,7 @@ const UserInfo = () => { dispatch(setUserSshKeyByIndex({ index: index, sshKey: value })); }; - const stepValidation = { - errors: {}, - disabledNext: false, - }; + const stepValidation = useUsersValidation(); return ( <> diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index d8c3f8a0f..807842536 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -20,6 +20,10 @@ import { selectRegistrationType, selectHostname, selectKernel, + selectUserNameByIndex, + selectUsers, + selectUserPasswordByIndex, + selectUserSshKeyByIndex, } from '../../../store/wizardSlice'; import { getDuplicateMountPoints, @@ -29,6 +33,7 @@ import { isSnapshotValid, isHostnameValid, isKernelNameValid, + isUserNameValid, } from '../validators'; export type StepValidation = { @@ -173,6 +178,31 @@ export function useKernelValidation(): StepValidation { return { errors: {}, disabledNext: false }; } +export function useUsersValidation(): StepValidation { + const index = 0; + const userNameSelector = selectUserNameByIndex(index); + const userName = useAppSelector(userNameSelector); + const userPasswordSelector = selectUserPasswordByIndex(index); + const userPassword = useAppSelector(userPasswordSelector); + const userSshKeySelector = selectUserSshKeyByIndex(index); + const userSshKey = useAppSelector(userSshKeySelector); + const users = useAppSelector(selectUsers); + const canProceed = + // Case 1: there is no users + users.length === 0 || + // Case 2: All fields are empty + (userName === '' && userPassword === '' && userSshKey === '') || + // Case 3: userName is valid + (userName && isUserNameValid(userName)); + + return { + errors: { + userName: !isUserNameValid(userName) ? 'Invalid user name' : '', + }, + disabledNext: !canProceed, + }; +} + export function useDetailsValidation(): StepValidation { const name = useAppSelector(selectBlueprintName); const description = useAppSelector(selectBlueprintDescription); diff --git a/src/Components/CreateImageWizard/validators.ts b/src/Components/CreateImageWizard/validators.ts index 05e6a2f8b..c9edc905f 100644 --- a/src/Components/CreateImageWizard/validators.ts +++ b/src/Components/CreateImageWizard/validators.ts @@ -57,6 +57,17 @@ export const isFileSystemConfigValid = (partitions: Partition[]) => { return duplicates.length === 0; }; +export const isUserNameValid = (userName: string) => { + if (userName === undefined) return false; + const isLengthValid = userName.length <= 32; + const isNotNumericOnly = !/^\d+$/.test(userName); + const isPatternValid = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9_$]$/.test( + userName + ); + + return isLengthValid && isNotNumericOnly && isPatternValid; +}; + export const getDuplicateMountPoints = (partitions: Partition[]): string[] => { const mountPointSet: Set = new Set(); const duplicates: string[] = []; diff --git a/src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx b/src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx index 4439b1c2e..813fed11b 100644 --- a/src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx @@ -72,6 +72,24 @@ const addValidUser = async () => { await waitFor(() => expect(nextButton).toBeEnabled()); }; +const addInvalidUser = async () => { + const user = userEvent.setup(); + const addUser = await screen.findByRole('button', { name: /add a user/i }); + expect(addUser).toBeEnabled(); + await waitFor(() => user.click(addUser)); + const enterUserName = screen.getByRole('textbox', { + name: /blueprint user name/i, + }); + const nextButton = await getNextButton(); + await waitFor(() => user.type(enterUserName, '..')); + await waitFor(() => expect(enterUserName).toHaveValue('..')); + const enterSshKey = await screen.findByRole('textbox', { + name: /public SSH key/i, + }); + await waitFor(() => user.type(enterSshKey, 'ssh-rsa d')); + await waitFor(() => expect(nextButton).toBeDisabled()); +}; + describe('Step Users', () => { beforeEach(async () => { vi.clearAllMocks(); @@ -145,6 +163,16 @@ describe('Step Users', () => { expect(receivedRequest).toEqual(expectedRequest); }); }); + + test('with invalid name', async () => { + await renderCreateMode(); + await goToRegistrationStep(); + await clickRegisterLater(); + await goToUsersStep(); + await addInvalidUser(); + const invalidUserMessage = screen.getByText(/invalid user name/i); + await waitFor(() => expect(invalidUserMessage)); + }); }); describe('Users edit mode', () => {