Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

papercut-fix: perform login on enter button press for basic auth in the login page #434

Merged
merged 1 commit into from
Apr 2, 2024
Merged
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
152 changes: 141 additions & 11 deletions src/__tests__/LoginPage/SignIn.test.js
Original file line number Diff line number Diff line change
@@ -24,14 +24,14 @@ afterEach(() => {

describe('Signin component automatic navigation', () => {
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');
});

it('navigates to homepage when auth is disabled', async () => {
// mock request to check auth
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { http: {} } });
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
});
@@ -48,7 +48,7 @@ describe('Sign in form', () => {
});

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 passwordInput = await screen.findByLabelText(/^Enter Password/i);
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 () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);
const usernameInput = await screen.findByLabelText(/^Username/i);
const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.click(usernameInput);
@@ -74,24 +74,154 @@ describe('Sign in form', () => {
await waitFor(() => expect(passwordError).toBeInTheDocument());
});

it('should log in the user and navigate to homepage if login is successful', async () => {
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
const submitButton = await screen.findByText('Continue');
it('should log in the user and navigate to homepage if login is successful using button', 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: {} } });
const submitButton = await screen.findByText('Continue');
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/home');
});
});

it('should display an error if username is blank and login is attempted using button', async () => {
render(<SignIn isLoggedIn={false} setIsLoggedIn={() => {}} />);

const passwordInput = await screen.findByLabelText(/^Enter Password/i);
userEvent.type(passwordInput, 'test');
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)).not.toBeInTheDocument());
await waitFor(() => {
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 isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
const submitButton = await screen.findByText('Continue');
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);
const errorDisplay = await screen.findByText(/Authentication Failed/i);

await waitFor(() => {
expect(screen.queryByText(/Authentication Failed/i)).toBeInTheDocument();
});
await waitFor(() => {
expect(errorDisplay).toBeInTheDocument();
expect(mockedUsedNavigate).not.toHaveBeenCalled();
});
});
});
71 changes: 53 additions & 18 deletions src/components/Login/SignIn.jsx
Original file line number Diff line number Diff line change
@@ -149,8 +149,8 @@ const useStyles = makeStyles(() => ({
export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading = () => {} }) {
const [usernameError, setUsernameError] = useState(null);
const [passwordError, setPasswordError] = useState(null);
const [username, setUsername] = useState(null);
const [password, setPassword] = useState(null);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [requestProcessing, setRequestProcessing] = useState(false);
const [requestError, setRequestError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
@@ -228,13 +228,20 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
});
};

const handleClick = (event) => {
event.preventDefault();
if (Object.keys(authMethods).includes('htpasswd')) {
const handleBasicAuthSubmit = () => {
setRequestError(false);
const isUsernameValid = handleUsernameValidation(username);
const isPasswordValid = handlePasswordValidation(password);
if (Object.keys(authMethods).includes('htpasswd') && isUsernameValid && isPasswordValid) {
handleBasicAuth();
}
};

const handleClick = (event) => {
event.preventDefault();
handleBasicAuthSubmit();
};

const handleGuestClick = () => {
setRequestProcessing(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) => {
event.preventDefault();
setRequestError(false);

const val = event.target?.value;
const isEmpty = val === '';

switch (type) {
case 'username':
setUsername(val);
if (isEmpty) {
setUsernameError('Please enter a username');
} else {
setUsernameError(null);
}
handleUsernameValidation(val);
break;
case 'password':
setPassword(val);
if (isEmpty) {
setPasswordError('Please enter a password');
} else {
setPasswordError(null);
}
handlePasswordValidation(val);
break;
default:
break;
}
};

const handleLoginInputFieldKeyDown = (event) => {
const keyPressed = event.key;
if (keyPressed === 'Enter') {
handleBasicAuthSubmit();
}
};

const renderThirdPartyLoginMethods = () => {
let isGoogle = isObject(authMethods.openid?.providers?.google);
// 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).includes('openid') &&
Object.keys(authMethods.openid.providers).length > 0 && (
<Divider className={classes.divider} data-testId="openid-divider">
<Divider className={classes.divider} data-testid="openid-divider">
or
</Divider>
)}
@@ -334,6 +361,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
onInput={(e) => handleChange(e, 'username')}
error={usernameError != null}
helperText={usernameError}
onKeyDown={(e) => handleLoginInputFieldKeyDown(e)}
/>
<TextField
margin="normal"
@@ -349,6 +377,7 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
onInput={(e) => handleChange(e, 'password')}
error={passwordError != null}
helperText={passwordError}
onKeyDown={(e) => handleLoginInputFieldKeyDown(e)}
/>
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
{requestError && (
@@ -357,7 +386,13 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
</Alert>
)}
<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
</Button>
</div>