HTTP Requests
Part 2, Chapter 7
At this point, we have two views designed to gather information from users for authentication. Users register for a new account using the “Sign Up” view and they log into an existing account using the “Log In” view. Each of our views is like a cell phone without a carrier. You can type something on the screen, but nothing receives anything from it or sends anything to it. In this chapter, we’re going to connect our interface to the “world” by wiring it up to the server. Once connected, users will be able to manipulate data on the server, which other app users can see.
Log In
The first aspect of authentication we want to handle is logging in. Given that the user has a valid active account, she should be able to enter her credentials and gain access to it. What should happen to the user interface when the user logs in? For one, she should be able to read, create, update, and delete her personal data. But we’re not there yet. For now, she should be able to log out.
Let’s create a test to prove that a user is logged in by finding a “Log out” button. Open the authentication.spec.js Cypress test file and edit the login test to look for a “Log out” button. It’s presence will confirm that the user is authenticated.
// cypress/integration/authentication.spec.js
it('Can log in.', function () {
cy.visit('/#/log-in');
cy.get('input#username').type('gary.cole@example.com');
cy.get('input#password').type('pAssw0rd', { log: false });
cy.get('button').contains('Log in').click();
cy.hash().should('eq', '#/');
// new
cy.get('button').contains('Log out');
});
If you run this test right now, it will fail since we haven’t set up our app to include a “Log out” button. Next, let’s add a couple elements to our main App.js file. We’ll add a hook to set a variable that indicates whether a user is logged into the app. We’ll also create the shell of a logIn()
method. We’ll modify the navigation bar to conditionally include a “Log out” button when the user is authenticated. Lastly, we’ll pass our logIn()
method to our LogIn
component so it can use it to set the logged in state for the entire app.
// src/App.js
import React, { useState } from 'react'; // changed
import {
Button, Container, Form, Navbar
} from 'react-bootstrap'; // changed
import { LinkContainer } from 'react-router-bootstrap';
import { Link, Route, Switch } from 'react-router-dom';
import SignUp from './components/SignUp';
import LogIn from './components/LogIn';
import './App.css';
function App () {
// new
const [isLoggedIn, setLoggedIn] = useState(false);
// new
const logIn = (username, password) => setLoggedIn(true);
return (
<div>
<Navbar bg='light' expand='lg' variant='light'>
<LinkContainer to='/'>
<Navbar.Brand className='logo'>Taxi</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle />
<Navbar.Collapse>
{/* new */}
{
isLoggedIn &&
<Form inline className='ml-auto'>
<Button type='button'>Log out</Button>
</Form>
}
</Navbar.Collapse>
</Navbar>
<Container className='pt-3'>
<Switch>
<Route exact path='/' render={() => (
<div className='middle-center'>
<h1 className='landing logo'>Taxi</h1>
<Link className='btn btn-primary' to='/sign-up'>Sign up</Link>
<Link className='btn btn-primary' to='/log-in'>Log in</Link>
</div>
)} />
<Route path='/sign-up' component={SignUp} />
{/* changed */}
<Route path='/log-in' render={() => (
<LogIn logIn={logIn} />
)} />
</Switch>
</Container>
</div>
);
}
export default App;
Initially, our logIn()
method just sets the isLoggedIn
variable to true
. We’ll add the real functionality to this function later in the chapter, but for now let’s just confirm that the “Log out” button appears properly.
We need to hook up the logIn()
method to the LogIn
component form before our test will pass. Edit the onSubmit()
method in the components/LogIn.js file to call logIn()
when the user submits his credentials in the form.
// src/components/LogIn.js
const onSubmit = (values, actions) => {
props.logIn(values.username, values.password);
setSubmitted(true);
};
Run the Cypress tests. The login test should be working now.
Happy Path
Remember, all we did here was stub the logIn()
function to return the result we wanted. Let’s take it one step further. Let’s make an API call to the server.
React doesn’t come with a native HTTP service, so we have to install a third-party dependency. Axios is a popular choice in the React community and it’s the one we’ll be using.
Install Axios.
$ npm install axios --save
Before we continue, we need to add protection against Cross-Site Request Forgeries. Let’s add the following code to the src/index.js file to ensure that every HTTP request we make includes the proper headers. The required header values are defined in the Django documentation.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootswatch/dist/lumen/bootstrap.css';
import axios from 'axios'; // new
import { HashRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
axios.defaults.xsrfCookieName = 'csrftoken'; // new
axios.defaults.xsrfHeaderName = 'X-CSRFToken'; // new
ReactDOM.render(
<HashRouter>
<App />
</HashRouter>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Next, let’s change our login test to actually make an HTTP request to the server. Since we’re doing this in baby steps, let’s mock the server API to return the data we expect the real server to return.
Make the following changes to the login test.
// cypress/integration/authentication.spec.js
it('Can log in.', function () {
// new
cy.server();
cy.route({
method: 'POST',
url: '**/api/log_in/**',
status: 200,
response: {
'access': 'ACCESS_TOKEN',
'refresh': 'REFRESH_TOKEN'
}
}).as('logIn');
cy.visit('/#/log-in');
cy.get('input#username').type('gary.cole@example.com');
cy.get('input#password').type('pAssw0rd', { log: false });
cy.get('button').contains('Log in').click();
// new
cy.wait('@logIn');
cy.hash().should('eq', '#/');
cy.get('button').contains('Log out');
});
Cypress’s server()
method captures requests to the server, while the route()
method can used to either stub server calls or actually pass them to the server. In this case, we’re capturing any POST
requests to the /api/log_in/
endpoint and returning a fake user’s data to the client. After we click the “Log in” button, we wait for our HTTP call to finish before checking our assertions. Nothing else in our test needs to change. We still expect to see the “Log out” button appear when the user logs into the application successfully.
We need to make a couple of changes to the App.js code we added in the previous step. First, how can the client tell whether the user is already authenticated? Remember in Part 1 we elected to use JSON Web Token authentication, which works by issuing an access token in exchange for a user’s credentials. That access token must be sent back to the server in all subsequent requests, and it will verify the user’s identity until the token expires.
The access token includes a Base 64 encoded data payload, which in our case contains the JSON-serialized user data. Since we need to use both the token and the user’s information frequently during the user’s session, we need to store the data in the browser. When the server returns the access token on login, we can store that data locally using the browser’s localStorage API.
Edit the App.js file to change the hook and the logIn()
function.
// src/App.js
import axios from 'axios';
const [isLoggedIn, setLoggedIn] = useState(() => { // changed
return window.localStorage.getItem('taxi.auth') !== null;
});
const logIn = async (username, password) => { // changed
const url = '/api/log_in/';
try {
const response = await axios.post(url, { username, password });
window.localStorage.setItem(
'taxi.auth', JSON.stringify(response.data)
);
setLoggedIn(true);
}
catch (error) {
console.error(error);
}
};
We modified our hook to determine the value of isLoggedIn
based on whether the window.localStorage
object contained an authenticated data resource. If the data is present, we know the user is logged in. If it is missing, then the user is not authenticated.
We also changed the logIn()
function to make an HTTP request. Our edits made logIn()
asynchronous – it runs asynchronously on the event loop and uses an implicit Promise
to return its result. The await
keyword pauses the function until the Promise
is resolved or rejected. In our case, the logIn()
function pauses until the the Axios HTTP client receives a response from the server. If the call succeeds, then we store the response data in window.localStorage
.
Since logIn()
is asynchronous, the LogIn
component’s onSubmit()
function must wait for it to resolve before it can resolve itself, so we make it asynchronous as well.
// src/components/LogIn.js
// changed
const onSubmit = async (values, actions) => {
try {
await props.logIn(values.username, values.password);
setSubmitted(true);
}
catch (error) {
console.error(error);
}
};
Here’s something you might have guessed we needed to handle. A logged-in user shouldn’t be able to visit the “Log In” page while he is authenticated. He needs to log out first. Let’s write a test to prove this.
// cypress/integration/authentication.js
it('Cannot visit the login page when logged in.', function () {
const { username, password } = Cypress.env('credentials');
// Log in.
cy.server();
cy.route({
method: 'POST',
url: '**/api/log_in/**',
status: 200,
response: {
'access': 'ACCESS_TOKEN',
'refresh': 'REFRESH_TOKEN'
}
}).as('logIn');
cy.visit('/#/log-in')
cy.get('input#username').type(username)
cy.get('input#password').type(password, { log: false })
cy.get('button').contains('Log in').click()
cy.hash().should('eq', '#/')
cy.get('button').contains('Log out')
cy.wait('@logIn')
cy.visit('/#/log-in');
cy.hash().should('eq', '#/');
});
This test stubs the log_in
API to return an OK response with some fake data to simulate a successful login event. Then, the test client attempts to visit the “Log In” page, but it should be redirected back to the home page.
We’re using the Cypress.env()
API to access environment variables that are scoped to the current test spec file. We need to define these values in the cypress.json file.
// cypress.json
{
"baseUrl": "http://localhost:3000",
"env": {
"credentials": {
"username": "gary.cole@example.com",
"password": "pAssw0rd"
}
}
}
Visiting the “Log In” page should redirect the user home. We can achieve this behavior with a simple adjustment. Let’s check whether the user is logged in and then either redirect him home if he is or render the LogIn
component if he is not. Change the /log-in
route to the following.
// src/App.js
<Route path='/log-in' render={() => (
isLoggedIn ? (
<Redirect to='/' />
) : (
<LogIn logIn={logIn} />
)
)} />
Now that we’ve made this change, we need to refactor the LogIn
component too.
// src/components/LogIn.js
import React from 'react'; // changed
import { Formik } from 'formik';
import {
Breadcrumb, Button, Card, Col, Form, Row
} from 'react-bootstrap';
import { Link } from 'react-router-dom'; // changed
function LogIn (props) {
// changed
const onSubmit = async (values, actions) => {
try {
await props.logIn(values.username, values.password);
}
catch (error) {
console.error(error);
}
};
return (
<Row>
<Col lg={12}>
<Breadcrumb>
<Breadcrumb.Item href='/'>Home</Breadcrumb.Item>
<Breadcrumb.Item active>Log in</Breadcrumb.Item>
</Breadcrumb>
<Card>
<Card.Header>Log in</Card.Header>
<Card.Body>
<Formik
initialValues={{
username: '',
password: ''
}}
onSubmit={onSubmit}
render={({
handleChange,
handleSubmit,
values
}) => (
{/* hidden for clarity */}
)}
/>
</Card.Body>
<p className='mt-3 text-center'>
Don't have an account? <Link to='/sign-up'>Sign up!</Link>
</p>
</Card>
</Col>
</Row>
);
}
export default LogIn;
You should notice a couple things here. First, we removed the redirect. That’s handled by the App
component now, so we don’t need it anymore. With the redirect gone, we don’t need the isSubmitted
state anymore either. In fact, the presence of this state was causing an issue before. Because we were redirecting from within LogIn
, the LogIn
component was being unmounted before the asynchronous function had a chance to update App
's state. We were getting an error message about a memory leak in the console. Removing the redirect and companion code fixes that issue.
An authenticated user has no business signing up for a new account either.
// cypress/integration/authentication.js
it('Cannot visit the sign up page when logged in.', function () {
const { username, password } = Cypress.env('credentials');
cy.server();
cy.route({
method: 'POST',
url: '**/api/log_in/**',
status: 200,
response: {
'access': 'ACCESS_TOKEN',
'refresh': 'REFRESH_TOKEN'
}
}).as('logIn');
cy.visit('/#/log-in')
cy.get('input#username').type(username)
cy.get('input#password').type(password, { log: false })
cy.get('button').contains('Log in').click()
cy.hash().should('eq', '#/')
cy.get('button').contains('Log out')
cy.wait('@logIn')
cy.visit('/#/sign-up');
cy.hash().should('eq', '#/');
});
Make a similar change to the /sign-up
route in the same file.
// src/App.js
<Route path='/sign-up' render={() => (
isLoggedIn ? (
<Redirect to='/' />
) : (
<SignUp />
)
)} />
Don’t forget to add the Redirect
import at the top of the page.
Users who are logged in also shouldn’t be given links to the Log In and Sign Up pages.
// cypress/integration/authentication.js
it('Cannot see links when logged in.', function () {
const { username, password } = Cypress.env('credentials');
cy.server();
cy.route({
method: 'POST',
url: '**/api/log_in/**',
status: 200,
response: {
'access': 'ACCESS_TOKEN',
'refresh': 'REFRESH_TOKEN'
}
}).as('logIn');
cy.visit('/#/log-in')
cy.get('input#username').type(username)
cy.get('input#password').type(password, { log: false })
cy.get('button').contains('Log in').click()
cy.hash().should('eq', '#/')
cy.get('button').contains('Log out')
cy.wait('@logIn')
cy.get('button#signUp').should('not.exist');
cy.get('button#logIn').should('not.exist');
});
// src/App.js
<Route exact path='/' render={() => (
<div className='middle-center'>
<h1 className='landing logo'>Taxi</h1>
{
!isLoggedIn &&
<Link
id='signUp'
className='btn btn-primary'
to='/sign-up'
>Sign up</Link>
}
{
!isLoggedIn &&
<Link
id='logIn'
className='btn btn-primary'
to='/log-in'
>Log in</Link>
}
</div>
)} />
A Quick Refactoring
You’ve probably noticed that we’ve reused the same login code over and again throughout our tests. Let’s refactor that login code into a reusable function.
Add the following function to the top of the authentication.spec.js file:
// integration/authentication.spec.js
const logIn = () => {
const { username, password } = Cypress.env('credentials');
// Capture HTTP requests.
cy.server();
cy.route({
method: 'POST',
url: '**/api/log_in/**',
status: 200,
response: {
'access': 'ACCESS_TOKEN',
'refresh': 'REFRESH_TOKEN'
}
}).as('logIn');
// Log into the app.
cy.visit('/#/log-in');
cy.get('input#username').type(username);
cy.get('input#password').type(password, { log: false });
cy.get('button').contains('Log in').click();
cy.wait('@logIn');
};
Next, refactor the tests to use the new logIn()
function.
// integration/authentication.spec.js
const logIn = () => {
// Collapsed for clarity.
};
describe('Authentication', function () {
it('Can log in.', function () {
logIn();
cy.hash().should('eq', '#/');
});
// not changed.
it('Can sign up.', function () {
// Hidden for clarity.
});
it('Cannot visit the login page when logged in.', function () {
logIn();
cy.visit('/#/log-in');
cy.hash().should('eq', '#/');
});
it('Cannot visit the sign up page when logged in.', function () {
logIn();
cy.visit('/#/sign-up');
cy.hash().should('eq', '#/');
});
it('Cannot see links when logged in.', function () {
logIn();
cy.get('button#signUp').should('not.exist');
cy.get('button#logIn').should('not.exist');
});
});
Run all of the tests to confirm that they are passing.
Server Errors
We programmed how to handle the happy path in our LogIn
component. But what happens if the server returns an error? In most cases, the error will be caused by the user. For example, he may enter incorrect credentials. We want to alert the user in this case and ask him to try again.
Let’s set up a scenario to test a client error. Add the following test to the cypress/integration/authentication.spec.js file.
// cypress/integration/authentication.spec.js
it('Shows an alert on login error.', function () {
const { username, password } = Cypress.env('credentials');
cy.server();
cy.route({
method: 'POST',
url: '**/api/log_in/**',
status: 400,
response: {
'__all__': [
'Please enter a correct username and password. ' +
'Note that both fields may be case-sensitive.'
]
}
}).as('logIn');
cy.visit('/#/log-in');
cy.get('input#username').type(username);
cy.get('input#password').type(password, { log: false });
cy.get('button').contains('Log in').click();
cy.wait('@logIn');
cy.get('div.alert').contains(
'Please enter a correct username and password. ' +
'Note that both fields may be case-sensitive.'
);
cy.hash().should('eq', '#/log-in');
});
Run the test and watch it fail.
Let’s write the code to get the test to pass.
// src/components/LogIn.js
// changed
const onSubmit = async (values, actions) => {
try {
const { response, isError } = await props.logIn(
values.username,
values.password
);
if (isError) {
const data = response.response.data;
for (const value in data) {
actions.setFieldError(value, data[value].join(' '));
}
}
}
catch (error) {
console.error(error);
}
finally {
actions.setSubmitting(false);
}
}
Change the logIn()
function in App.js too.
// src/App.js
const logIn = async (username, password) => {
const url = '/api/log_in/';
try {
const response = await axios.post(url, { username, password });
window.localStorage.setItem(
'taxi.auth', JSON.stringify(response.data)
);
setLoggedIn(true);
// new
return { response, isError: false };
}
catch (error) {
console.error(error);
// new
return { response: error, isError: true };
}
};
When the user submits the form, the submit()
function will invoke the logIn()
function to send the credentials to the server. If the call fails, then the form will display the errors.
Note
Instead of using
props.logIn()
, we can referencelogIn()
directly by destructuring theprops
object into the keys we expect to receive.
// old
function LogIn(props) {...}
// new
function LogIn({ logIn }) {...}
Lastly, we need to update our Formik
component to display the errors correctly. Any errors that the server returns to us with the __all__
key are not related to a particular field but are related to the data as a whole. Given that information, we’re going to display general errors at the top of the page using a Bootstrap Alert component.
// src/components/LogIn.js
<Formik
initialValues={{
username: '',
password: ''
}}
onSubmit={onSubmit}
>
{({
errors, // new
handleChange,
handleSubmit,
isSubmitting, // new
values
}) => (
{/* new */}
<>
{
'__all__' in errors &&
<Alert variant='danger'>
{ errors['__all__'] }
</Alert>
}
<Form noValidate onSubmit={handleSubmit}>
{/* contents hidden for clarity */}
<Button
block
disabled={isSubmitting}
type='submit'
variant='primary'
>Log in</Button>
</Form>
</>
)}
</Formik>
In addition to the Alert
, we’re also updating the submit button to disable itself until the form submission Ajax request completes.
Don’t forget to add the Alert
import to the top of the page.
// src/components/LogIn.js
import {
Alert, Breadcrumb, Button, Card, Col, Form, Row // changed
} from '
With these changes in place, our tests should pass.
Log Out
Here’s what should happen in our client when an authenticated user logs out:
- The “Log out” button should disappear.
- The user’s data should be removed from
LocalStorage
. - The user should regain access to the Login Page .
Create a new failing test.
// cypress/integration/authentication.spec.js
it('Can log out.', function () {
logIn();
cy.get('button').contains('Log out').click().should(() => {
expect(window.localStorage.getItem('taxi.auth')).to.be.null;
});
cy.get('button').contains('Log out').should('not.exist');
});
In this test, we add mock data to LocalStorage
to make the “Log out” button appear on the screen. After confirming that the “Log out” button is on the page, we click it, and then we check whether the authentication data has been removed from LocalStorage
. Lastly, we confirm that the “Log out” button has been removed from the page.
Let’s add a logOut()
function and a “Log Out” button to our App
component.
// src/App.js
const logOut = () => {
window.localStorage.removeItem('taxi.auth');
setLoggedIn(false);
};
{/* changed */}
<Navbar.Collapse>
{
isLoggedIn && (
<Form inline className='ml-auto'>
<Button type='button' onClick={() => logOut()}>Log out</Button>
</Form>
)
}
</Navbar.Collapse>
With the code in place, clicking the “Log out” button will trigger to call to the server.
Our logout test should be passing.
Sign Up
Our last step is to implement the HTTP client in our sign up workflow. Modify the sign up test in the authentication test file like the following.
// cypress/integration/authentication.js
it('Can sign up.', function () {
// new
cy.server();
cy.route({
method: 'POST',
url: '**/api/sign_up/**',
status: 201,
response: {
'id': 1,
'username': 'gary.cole@example.com',
'first_name': 'Gary',
'last_name': 'Cole',
'group': 'driver',
'photo': '/media/images/photo.jpg'
}
}).as('signUp');
cy.visit('/#/sign-up');
cy.get('input#username').type('gary.cole@example.com');
cy.get('input#firstName').type('Gary');
cy.get('input#lastName').type('Cole');
cy.get('input#password').type('pAssw0rd', { log: false });
cy.get('select#group').select('driver');
// Handle file upload
cy.fixture('images/photo.jpg').then(photo => {
cy.get('input#photo').upload({
fileContent: photo,
fileName: 'photo.jpg',
mimeType: 'application/json'
});
});
cy.get('button').contains('Sign up').click();
cy.wait('@signUp'); // new
cy.hash().should('eq', '#/log-in');
});
Our code changes stub the server to return mock data. After we click the “Sign up” button, we wait for the HTTP call to resolve. And we confirm that the user is redirected to the Login Page .
Let’s alter the onSubmit()
function in our SignUp
component to call the server.
// src/components/SignUp.js
const onSubmit = async (values, actions) => {
const url = '/api/sign_up/';
const formData = new FormData();
formData.append('username', values.username);
formData.append('first_name', values.firstName);
formData.append('last_name', values.lastName);
formData.append('password1', values.password);
formData.append('password2', values.password);
formData.append('group', values.group);
formData.append('photo', values.photo);
try {
await axios.post(url, formData);
setSubmitted(true);
}
catch (response) {
const data = response.response.data;
for (const value in data) {
actions.setFieldError(value, data[value].join(' '));
}
}
finally {
actions.setSubmitting(false);
}
};
Add the axios
import at the top of the file too:
import axios from 'axios';
Unlike with our
logIn()
function, theApp
component’s state does not rely on a user signing up, so we can handle signing up directly in anonSubmit()
function on ourSignUp
component.
This HTTP call looks a bit different than the login and logout calls. Since we’re sending a photo in our request, we need to use the FormData JavaScript interface, which uses the same format a form would use if the encoding type were set to multipart/form-data
.
Check your tests and confirm that they’re all passing.
Server Errors
We’re going to follow a similar list of steps to handle sign up errors as we did for our login.
Let’s begin by writing a test for handling sign up errors.
// cypress/integration/authentication.spec.js
it('Show invalid fields on sign up error.', function () {
cy.server();
cy.route({
method: 'POST',
url: '**/api/sign_up/**',
status: 400,
response: {
'username': [
'A user with that username already exists.'
]
}
}).as('signUp');
cy.visit('/#/sign-up');
cy.get('input#username').type('gary.cole@example.com');
cy.get('input#firstName').type('Gary');
cy.get('input#lastName').type('Cole');
cy.get('input#password').type('pAssw0rd', { log: false });
cy.get('select#group').select('driver');
// Handle file upload
cy.fixture('images/photo.jpg').then(photo => {
cy.get('input#photo').upload({
fileContent: photo,
fileName: 'photo.jpg',
mimeType: 'application/json'
});
});
cy.get('button').contains('Sign up').click();
cy.wait('@signUp');
cy.get('div.invalid-feedback').contains(
'A user with that username already exists'
);
cy.hash().should('eq', '#/sign-up');
});
We capture outgoing client requests to the /api/sign_up/
API endpoint and return a mock client error.
Unlike our login form, our sign up form will have errors relating to specific fields. Let’s prepare our form to display those errors by first updating our Formik
component.
// src/components/SignUp.js
<Formik
initialValues={{
username: '',
firstName: '',
lastName: '',
password: '',
group: 'rider',
photo: []
}}
onSubmit={onSubmit}
>
{({
errors, // new
handleChange,
handleSubmit,
isSubmitting, // new
setFieldValue, // new
values
}) => (
{/* contents hidden for clarity */}
)}
</Formik>
We only want to show an error if we receive an error from the server. Let’s start by editing the code for the username
field to get a hang of the changes we need to make. Then we can apply the same change to the rest of the fields.
// src/components/SignUp.js
<Form.Group controlId='username'>
<Form.Label>Username:</Form.Label>
{/* changed */}
<Form.Control
className={ 'username' in errors ? 'is-invalid' : '' }
name='username'
onChange={handleChange}
values={values.username}
required
/>
{/* new */}
{
'username' in errors &&
<Form.Control.Feedback type='invalid'>{ errors.username }</Form.Control.Feedback>
}
</Form.Group>
Note that we need to update the Bootstrap form control class
to display a red border for an error. Also, we need to display feedback text under the form control with the error message.
Let’s apply the same change to the rest of the form controls in the sign up form.
// src/components/SignUp.js
<Form.Group controlId='firstName'>
<Form.Label>First name:</Form.Label>
<Form.Control
className={ 'firstName' in errors ? 'is-invalid' : '' }
name='firstName'
onChange={handleChange}
values={values.firstName}
/>
{
'firstName' in errors &&
<Form.Control.Feedback type='invalid'>{ errors.firstName }</Form.Control.Feedback>
}
</Form.Group>
<Form.Group controlId='lastName'>
<Form.Label>Last name:</Form.Label>
<Form.Control
className={ 'lastName' in errors ? 'is-invalid' : '' }
name='lastName'
onChange={handleChange}
values={values.lastName}
/>
{
'lastName' in errors &&
<Form.Control.Feedback type='invalid'>{ errors.lastName }</Form.Control.Feedback>
}
</Form.Group>
<Form.Group controlId='password'>
<Form.Label>Password:</Form.Label>
<Form.Control
className={ 'password' in errors ? 'is-invalid' : '' }
name='password'
onChange={handleChange}
type='password'
value={values.password}
/>
{
'password' in errors &&
<Form.Control.Feedback type='invalid'>{ errors.password }</Form.Control.Feedback>
}
</Form.Group>
<Form.Group controlId='group'>
<Form.Label>Group:</Form.Label>
<Form.Control
as='select'
className={ 'group' in errors ? 'is-invalid' : '' }
name='group'
onChange={handleChange}
value={values.group}
>
<option value='rider'>Rider</option>
<option value='driver'>Driver</option>
</Form.Control>
{
'group' in errors &&
<Form.Control.Feedback type='invalid'>{ errors.group }</Form.Control.Feedback>
}
</Form.Group>
<Form.Group controlId='photo'>
<Form.Label>Photo:</Form.Label>
<Form.Control
className={ 'photo' in errors ? 'is-invalid' : '' }
name='photo'
onChange={event => {
setFieldValue('photo', event.currentTarget.files[0]);
}}
type='file'
/>
{
'photo' in errors &&
<Form.Control.Feedback type='invalid'>{ errors.photo }</Form.Control.Feedback>
}
</Form.Group>
Run the Cypress tests one more time to see them working.