Skip to content
This repository has been archived by the owner on Jan 18, 2023. It is now read-only.

Feature/password validation #118

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 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
31 changes: 8 additions & 23 deletions src/components/Objects/Input/Input.jsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
import React from "react";

export default function Input({
className,
label,
onValueChange,
errorMessage,
type='text'
}) {

/************************************
* Helper Functions
************************************/

function handleChange(event) {
onValueChange(event.target.value);
}
import React from 'react';

export default function Input({ className, label, onValueChange, errorMessage, type = 'text', ...rest }) {
/************************************
* Render
************************************/

return (
<>
<label className={`${className}-input-container`}>
<div className='label-text-container'>
<p>{label}</p>
{errorMessage && <span>{errorMessage}</span>}
</div>
<input aria-label={`${label}-input`} type={type} onChange={handleChange}/>
<div className='label-text-container'>
<p>{label}</p>
{errorMessage && <span>{errorMessage}</span>}
</div>
<input {...rest} aria-label={`${label}-input`} type={type} onChange={onValueChange} />
</label>
</>
);
};
}
12 changes: 6 additions & 6 deletions src/constants/authentication-constants.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const SUCCESS = "SUCCESS";
export const SUCCESS = 'SUCCESS';

export const ERROR_INVALID_EMAIL = "You have entered an invalid email address";
export const ERROR_EMPTY_EMAIL = "You must enter an email address";
export const ERROR_EMPTY_PASSWORD = "You must enter a password";
export const ERROR_EMPTY_REENTERED_PASSWORD = "You must reenter the password";
export const ERROR_PASSWORDS_DONT_MATCH = "Entered passwords are not the same";
export const ERROR_INVALID_EMAIL = 'You have entered an invalid email address';
export const ERROR_EMPTY_EMAIL = 'You must enter an email address';
export const ERROR_EMPTY_PASSWORD = 'You must enter a password';
export const ERROR_EMPTY_REENTERED_PASSWORD = 'You must reenter the password';
export const ERROR_MISMATCH_PASSWORDS = 'Entered passwords are not the same';
Fabricevladimir marked this conversation as resolved.
Show resolved Hide resolved
64 changes: 64 additions & 0 deletions src/hooks/__tests__/useForm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { renderHook, act } from '@testing-library/react-hooks';

import useForm from '../useForm';
import { Schema } from '../../validation';

const LAST_NAME = 'last';
const FIRST_NAME = 'first';
const EVENT = { target: { name: FIRST_NAME, value: 'def' }, preventDefault: jest.fn() };

const VALID_FORM = { [FIRST_NAME]: 'abcd', [LAST_NAME]: 'abcd' };
const DEFAULT_STATE = { [FIRST_NAME]: '', [LAST_NAME]: '' };
const DEFAULT_SCHEMA = { [FIRST_NAME]: new Schema().min(4).validate(), last: new Schema().validate() };

describe('useForm hook', () => {
function setup(schema = DEFAULT_SCHEMA, state = DEFAULT_STATE) {
const { result } = renderHook(() => useForm(schema, state));

const validateProperty = jest.fn();
return { result, validateProperty };
}

test('should set no errors by default and form data from state', () => {
const { result } = setup();

expect(result.current.formErrors).toStrictEqual({});
expect(result.current.formData).toStrictEqual(DEFAULT_STATE);
});

test('should set proper element value and call validation function', () => {
const { result, validateProperty } = setup();

act(() => {
result.current.handleInputChange(EVENT, validateProperty);
});

const { value, name: propertyName } = EVENT.target;
const schema = DEFAULT_SCHEMA[propertyName];

expect(result.current.formData[FIRST_NAME]).toBe(EVENT.target.value);
expect(validateProperty).toHaveBeenCalledWith(value, schema, propertyName);
});

test('should call user submit callback when form is valid', () => {
const { result, validateProperty: validateForm } = setup(undefined, VALID_FORM);
const doSubmit = jest.fn().mockResolvedValue(true);

act(() => {
result.current.handleSubmit(EVENT, validateForm, doSubmit);
});

expect(doSubmit).toHaveBeenCalled();
expect(result.current.submitErrorMessage).toBeNull();
});

test('should set submitErrorMessage when error occurs while submitting valid form', () => {
const { result, validateProperty: validateForm } = setup(undefined, VALID_FORM);

act(() => {
result.current.handleSubmit(EVENT, validateForm);
});

expect(result.current.submitErrorMessage).not.toBeNull();
});
});
66 changes: 66 additions & 0 deletions src/hooks/useForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState } from 'react';

/**
* Hook to handle underlying form functionality such as setting input changes,
* errors, form and property validation and form submission
* @param {Object} schema schema of whole form
* @param {Object} defaultState object with properties corresponding to form elements
* @param {Object} defaultErrors form errors
*/
export default function useForm(schema, defaultState, defaultErrors = {}) {
/************************************
* State
************************************/

const [formData, setFormData] = useState(defaultState);
const [formErrors, setFormErrors] = useState(defaultErrors);
const [submitErrorMessage, setSubmitErrorMessage] = useState(null);

/************************************
* Helper Functions
************************************/

/**
* Handles validating form and calling submitCallback
* @param {Event} event form submit event
* @param {Function} validateForm user validation function to be called
* @param {Function} submitCallback async function that does actual submission
*/
async function handleSubmit(event, validateForm, submitCallback) {
event.preventDefault();

const errors = validateForm(formData, schema);
if (!errors) {
try {
await submitCallback();
} catch (error) {
setSubmitErrorMessage(error.message);
}
} else {
setFormErrors({ ...formErrors, ...errors });
}
}

/**
* Sets the appropriate state and errors. Validates the property
* and allows user to run further validation
* @param {Event} event onChange event
* @param {Function} validateProperty user function for further
* validation. Must return an array of errors
*/
function handleInputChange(event, validateProperty) {
const { name: propertyName, value } = event.target;

setFormData({ ...formData, [propertyName]: value });
validateProperty(value, schema[propertyName], propertyName);
}

return {
formData,
formErrors,
handleSubmit,
setFormErrors,
handleInputChange,
submitErrorMessage
};
}
117 changes: 48 additions & 69 deletions src/pages/LoginView.jsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,61 @@
import React, { useState } from 'react';
import React from 'react';
import { Redirect, withRouter } from 'react-router-dom';

import Input from '../components/Objects/Input/Input';
import useForm from '../hooks/useForm';
import homeLogo from '../images/homeLogo.png';
import { login } from '../services/authService';
import { useUser } from '../contexts/UserContext';
import { ERROR_EMPTY_EMAIL, ERROR_EMPTY_PASSWORD } from '../constants/authentication-constants';
import { Schema, validate } from '../validation';

const EMAIL_SCHEMA = new Schema().validate();
const EMAIL_PROPERTY = 'email';
const PASSWORD_SCHEMA = new Schema().validate();
const PASSWORD_PROPERTY = 'password';
const DEFAULT_STATE = { [EMAIL_PROPERTY]: '', [PASSWORD_PROPERTY]: '' };
const FORM_SCHEMA = {
[EMAIL_PROPERTY]: EMAIL_SCHEMA,
[PASSWORD_PROPERTY]: PASSWORD_SCHEMA
};

function LoginView(props) {
/************************************
* State
************************************/

const [user, setUser] = useUser();
const [errorMessage, setErrorMessage] = useState(null);

const [email, setEmail] = useState('');
const [emailErrorMessage, setEmailErrorMessage] = useState(null);
const [password, setPassword] = useState('');
const [passwordErrorMessage, setPasswordErrorMessage] = useState(null);
const { formData, formErrors, setFormErrors, submitErrorMessage, handleSubmit, handleInputChange } = useForm(
FORM_SCHEMA,
DEFAULT_STATE
);

/************************************
* Helper Functions
************************************/

/* Handler for log in button clicked, if valid inputs logs in user
*/
async function loginClicked() {
if (validateInput()) {
try {
// Ideally login should handle setting the user
const userData = await login(email, password);
setUser(userData);

// Make sure that userData is safely stored since this
// does a *full* reload
window.location = props.location.state?.referer || '/';
} catch (error) {
// This assumes that the server returns custom validation errors
// 500 errors should be handled and "prettied" in the httpService
setErrorMessage(error.message);
}
}
}

/* Validates inputs, if invalid then it will display an error message
*/
function validateInput() {
handleEmailValidationResponse(email);
handlePasswordValidationResponse(password);

return email !== '' && password !== '';
}
async function doSubmit() {
const userData = await login(formData[EMAIL_PROPERTY], formData[PASSWORD_PROPERTY]);
setUser(userData);

function emailValidationCheck(email) {
setEmail(email);
handleEmailValidationResponse(email);
// Make sure that userData is safely stored since this
// does a *full* reload
window.location = props.location.state?.referer || '/';
}

function passwordValidationCheck(password) {
setPassword(password);
handlePasswordValidationResponse(password);
// per useForm docs, must return object with each property
// containing an array of error messages or null if no errors
function validateForm(form, schema) {
const { isValid, errors } = validate(form, schema);
return isValid ? null : errors;
}

/* Takes in validation response of email and sets based on success or not
*
* @param response
*/
function handleEmailValidationResponse(input) {
const message = input === '' ? ERROR_EMPTY_EMAIL : null;
setEmailErrorMessage(message);
}

/* Takes in validation response of password and sets based on success or not
*
* @param response
*/
function handlePasswordValidationResponse(input) {
const message = input === '' ? ERROR_EMPTY_PASSWORD : null;
setPasswordErrorMessage(message);
// Per useForm docs, must return array of error messages
function validateProperty(value, schema, propertyName) {
const { errors } = validate(value, schema);
setFormErrors({
...formErrors,
[propertyName]: [...errors]
});
}

/************************************
Expand All @@ -90,26 +67,28 @@ function LoginView(props) {
<div className='authentication-view-body'>
<div className='authentication-input-container'>
<img src={homeLogo} alt='oneleif logo' />
<div className='form-container'>
<form onSubmit={event => handleSubmit(event, validateForm, doSubmit)} className='form-container'>
<Input
className='auth'
name={EMAIL_PROPERTY}
label='Email'
onValueChange={email => emailValidationCheck(email)}
errorMessage={emailErrorMessage}
className='auth'
errorMessage={formErrors[EMAIL_PROPERTY]}
onValueChange={event => handleInputChange(event, validateProperty)}
/>
<Input
className='auth'
label='Password'
type='password'
onValueChange={password => passwordValidationCheck(password)}
errorMessage={passwordErrorMessage}
name={PASSWORD_PROPERTY}
label='Password'
className='auth'
errorMessage={formErrors[PASSWORD_PROPERTY]}
onValueChange={event => handleInputChange(event, validateProperty)}
/>
<div className='authentication-actions-module'>
<span>Forgot your password?</span>
<button onClick={() => loginClicked()}>Log in</button>
<button type='submit'>Log in</button>
</div>
</div>
{errorMessage && <p className='error-message'>{errorMessage}</p>}
</form>
{submitErrorMessage && <p className='error-message'>{submitErrorMessage}</p>}
</div>
</div>
);
Expand Down
Loading