papercut-fix: perform login on enter button press for basic auth (#434)

Signed-off-by: Vishwas Rajashekar <vrajashe@cisco.com>
This commit is contained in:
Vishwas R 2024-04-02 18:25:08 +05:30 committed by Alexander Burmatov
parent 743d4fe073
commit 8311d59e53
2 changed files with 196 additions and 31 deletions

View File

@ -24,14 +24,14 @@ afterEach(() => {
describe('Signin component automatic navigation', () => { describe('Signin component automatic navigation', () => {
it('navigates to homepage when user is already logged in', async () => { it('navigates to homepage when user is already logged in', async () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={true} setIsLoggedIn={() => {}} />); render(<SignIn isLoggedIn={true} setIsLoggedIn={() => {}} />);
await expect(mockedUsedNavigate).toHaveBeenCalledWith('/home'); await expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
}); });
it('navigates to homepage when auth is disabled', async () => { it('navigates to homepage when auth is disabled', async () => {
// mock request to check auth // mock request to check auth
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { http: {} } }); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { http: {} } });
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />); render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
await waitFor(() => { await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home'); expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
}); });
@ -48,7 +48,7 @@ describe('Sign in form', () => {
}); });
it('should change username and password values on user input', async () => { it('should change username and password values on user input', async () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />); render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i); const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i); const passwordInput = await screen.findByLabelText(/^Enter Password/i);
fireEvent.change(usernameInput, { target: { value: 'test' } }); fireEvent.change(usernameInput, { target: { value: 'test' } });
@ -59,7 +59,7 @@ describe('Sign in form', () => {
}); });
it('should display error if username and password values are empty after change', async () => { it('should display error if username and password values are empty after change', async () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />); render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i); const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i); const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.click(usernameInput); userEvent.click(usernameInput);
@ -74,24 +74,154 @@ describe('Sign in form', () => {
await waitFor(() => expect(passwordError).toBeInTheDocument()); await waitFor(() => expect(passwordError).toBeInTheDocument());
}); });
it('should log in the user and navigate to homepage if login is successful', async () => { it('should log in the user and navigate to homepage if login is successful using button', async () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />); render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const submitButton = await screen.findByText('Continue');
const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(usernameInput, 'test');
userEvent.type(passwordInput, 'test');
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } }); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
const submitButton = await screen.findByText('Continue');
fireEvent.click(submitButton); fireEvent.click(submitButton);
await waitFor(() => { await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home'); expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
}); });
}); });
it('should should display login error if login not successful', async () => { it('should display an error if username is blank and login is attempted using button', async () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />); render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const submitButton = await screen.findByText('Continue');
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} }); const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(passwordInput, 'test');
const submitButton = await screen.findByTestId('basic-auth-submit-btn');
fireEvent.click(submitButton); fireEvent.click(submitButton);
const errorDisplay = await screen.findByText(/Authentication Failed/i);
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
await waitFor(() => expect(screen.queryByText(/enter a password/i)).not.toBeInTheDocument());
await waitFor(() => { await waitFor(() => {
expect(errorDisplay).toBeInTheDocument(); expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
it('should display an error if password is blank and login is attempted using button', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i);
userEvent.type(usernameInput, 'test');
const submitButton = await screen.findByTestId('basic-auth-submit-btn');
fireEvent.click(submitButton);
await waitFor(() => expect(screen.queryByText(/enter a username/i)).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
await waitFor(() => {
expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
it('should display an error if username and password are both blank and login is attempted using button', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const submitButton = await screen.findByTestId('basic-auth-submit-btn');
fireEvent.click(submitButton);
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
await waitFor(() => {
expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
it('should log in the user and navigate to homepage if login is successful using enter key on username field', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(usernameInput, 'test');
userEvent.type(passwordInput, 'test');
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
userEvent.type(usernameInput, '{enter}');
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
});
});
it('should log in the user and navigate to homepage if login is successful using enter key on password field', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(usernameInput, 'test');
userEvent.type(passwordInput, 'test');
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {} } });
userEvent.type(passwordInput, '{enter}');
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
});
});
it('should display an error if username is blank and login is attempted using enter key', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(passwordInput, 'test');
userEvent.type(passwordInput, '{enter}');
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
await waitFor(() => expect(screen.queryByText(/enter a password/i)).not.toBeInTheDocument());
await waitFor(() => {
expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
it('should display an error if password is blank and login is attempted using enter key', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i);
userEvent.type(usernameInput, 'test');
userEvent.type(usernameInput, '{enter}');
await waitFor(() => expect(screen.queryByText(/enter a username/i)).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
await waitFor(() => {
expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
it('should display an error if username and password are both blank and login is attempted using enter key', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(passwordInput, '{enter}');
await waitFor(() => expect(screen.queryByText(/enter a username/i)).toBeInTheDocument());
await waitFor(() => expect(screen.queryByText(/enter a password/i)).toBeInTheDocument());
await waitFor(() => {
expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
it('should should display login error if login not successful', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(usernameInput, 'test');
userEvent.type(passwordInput, 'test');
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
const submitButton = await screen.findByText('Continue');
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.queryByText(/Authentication Failed/i)).toBeInTheDocument();
});
await waitFor(() => {
expect(mockedUsedNavigate).not.toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -149,8 +149,8 @@ const useStyles = makeStyles(() => ({
export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = () => {} }) { export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = () => {} }) {
const [usernameError, setUsernameError] = useState(null); const [usernameError, setUsernameError] = useState(null);
const [passwordError, setPasswordError] = useState(null); const [passwordError, setPasswordError] = useState(null);
const [username, setUsername] = useState(null); const [username, setUsername] = useState('');
const [password, setPassword] = useState(null); const [password, setPassword] = useState('');
const [requestProcessing, setRequestProcessing] = useState(false); const [requestProcessing, setRequestProcessing] = useState(false);
const [requestError, setRequestError] = useState(false); const [requestError, setRequestError] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -228,13 +228,20 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
}); });
}; };
const handleClick = (event) => { const handleBasicAuthSubmit = () => {
event.preventDefault(); setRequestError(false);
if (Object.keys(authMethods).includes('htpasswd')) { const isUsernameValid = handleUsernameValidation(username);
const isPasswordValid = handlePasswordValidation(password);
if (Object.keys(authMethods).includes('htpasswd') && isUsernameValid && isPasswordValid) {
handleBasicAuth(); handleBasicAuth();
} }
}; };
const handleClick = (event) => {
event.preventDefault();
handleBasicAuthSubmit();
};
const handleGuestClick = () => { const handleGuestClick = () => {
setRequestProcessing(false); setRequestProcessing(false);
setRequestError(false); setRequestError(false);
@ -251,35 +258,55 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
); );
}; };
const handleUsernameValidation = (username) => {
let isValid = true;
if (username === '') {
setUsernameError('Please enter a username');
isValid = false;
} else {
setUsernameError(null);
}
return isValid;
};
const handlePasswordValidation = (password) => {
let isValid = true;
if (password === '') {
setPasswordError('Please enter a password');
isValid = false;
} else {
setPasswordError(null);
}
return isValid;
};
const handleChange = (event, type) => { const handleChange = (event, type) => {
event.preventDefault(); event.preventDefault();
setRequestError(false); setRequestError(false);
const val = event.target?.value; const val = event.target?.value;
const isEmpty = val === '';
switch (type) { switch (type) {
case 'username': case 'username':
setUsername(val); setUsername(val);
if (isEmpty) { handleUsernameValidation(val);
setUsernameError('Please enter a username');
} else {
setUsernameError(null);
}
break; break;
case 'password': case 'password':
setPassword(val); setPassword(val);
if (isEmpty) { handlePasswordValidation(val);
setPasswordError('Please enter a password');
} else {
setPasswordError(null);
}
break; break;
default: default:
break; break;
} }
}; };
const handleLoginInputFieldKeyDown = (event) => {
const keyPressed = event.key;
if (keyPressed === 'Enter') {
handleBasicAuthSubmit();
}
};
const renderThirdPartyLoginMethods = () => { const renderThirdPartyLoginMethods = () => {
let isGoogle = isObject(authMethods.openid?.providers?.google); let isGoogle = isObject(authMethods.openid?.providers?.google);
// let isGitlab = isObject(authMethods.openid?.providers?.gitlab); // let isGitlab = isObject(authMethods.openid?.providers?.gitlab);
@ -315,7 +342,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
{Object.keys(authMethods).length > 1 && {Object.keys(authMethods).length > 1 &&
Object.keys(authMethods).includes('openid') && Object.keys(authMethods).includes('openid') &&
Object.keys(authMethods.openid.providers).length > 0 && ( Object.keys(authMethods.openid.providers).length > 0 && (
<Divider className={classes.divider} data-testId="openid-divider"> <Divider className={classes.divider} data-testid="openid-divider">
or or
</Divider> </Divider>
)} )}
@ -334,6 +361,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
onInput={(e) => handleChange(e, 'username')} onInput={(e) => handleChange(e, 'username')}
error={usernameError != null} error={usernameError != null}
helperText={usernameError} helperText={usernameError}
onKeyDown={(e) => handleLoginInputFieldKeyDown(e)}
/> />
<TextField <TextField
margin="normal" margin="normal"
@ -349,6 +377,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
onInput={(e) => handleChange(e, 'password')} onInput={(e) => handleChange(e, 'password')}
error={passwordError != null} error={passwordError != null}
helperText={passwordError} helperText={passwordError}
onKeyDown={(e) => handleLoginInputFieldKeyDown(e)}
/> />
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />} {requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
{requestError && ( {requestError && (
@ -357,7 +386,13 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
</Alert> </Alert>
)} )}
<div> <div>
<Button fullWidth variant="contained" className={classes.continueButton} onClick={handleClick}> <Button
fullWidth
variant="contained"
className={classes.continueButton}
onClick={handleClick}
data-testid="basic-auth-submit-btn"
>
Continue Continue
</Button> </Button>
</div> </div>