This is a documentation for using a simple HTML login web form that is integrated with an Express JS server. In addition to that, a Cypress test is created to automate the process of testing this application.
The Express JS Server code
const minimist = require('minimist')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const session = require('express-session')
const express = require('express')
const app = express()
// get port from passed in args from scripts/start.js
const port = minimist(process.argv.slice(2)).port
const matchesUsernameAndPassword = (body = {}) => {
return body.username === 'jane.lane' && body.password === 'password123'
}
const ensureLoggedIn = (req, res, next) => {
if (req.session.user) {
next()
} else {
res.redirect('/unauthorized')
}
}
// parse regular form submission bodies
const urlencodedParser = bodyParser.urlencoded({ extended: false })
app.use(morgan('dev'))
// store a session cookie called
// 'cypress-session-cookie'
app.use(session({
name: 'cypress-session-cookie',
secret: 'sekret',
resave: false,
saveUninitialized: false,
}))
// app.use((req, res, next) => {
// console.log('session info is', req.session)
// next()
// })
app.set('views', __dirname)
app.set('view engine', 'hbs')
app.get('/', (req, res) => res.redirect('/login'))
// this is the standard HTML login page
app.get('/login', (req, res) => {
res.render('./login.hbs')
})
// specify that the urlencodedParser should only
// be used on this one route when its coming from
// the login form
app.post('/login', urlencodedParser, (req, res) => {
// if this matches the secret username and password
if (matchesUsernameAndPassword(req.body)) {
req.session.user = 'jane.lane'
res.redirect('/dashboard')
} else {
// render login with errors
res.render('./login.hbs', {
error: 'Username and/or password is incorrect',
})
}
})
app.get('/dashboard', ensureLoggedIn, (req, res) => {
res.render('./dashboard.hbs', {
user: req.session.user,
})
})
app.get('/users', ensureLoggedIn, (req, res) => {
res.render('./users.hbs')
})
app.get('/admin', ensureLoggedIn, (req, res) => {
res.render('./admin.hbs')
})
app.get('/unauthorized', (req, res) => {
res.render('./unauthorized.hbs')
})
app.listen(port)
Step to set up the project for running this server code
- Open Visual Studio Code and create a project folder.
- Make sure that Node.js is installed on your device
- Create a package.json
npm init -y - Install Dependencies
npm install express minimist morgan body-parser express-session hbs - Create a new file named
server.js
in the root of your project folder. - Paste the provided Express server code into
server.js
. - You should be now ready to run the local server with the following command:
npm start — –port=7077 - Open your browser and navigate to:
http://localhost:7077 - By default it will redirect you to the login page:
http://localhost:7077/login - Test the login functionality:
Use the usernamejane.lane
and passwordpassword123
to log in. - Successful login should lead you to the dashboard page:
http://localhost:7077/dashboard
Express JS Middlewares used in this server code:
1. minimist
- What it does:
minimist
is a lightweight library used to parse command-line arguments passed to a Node.js application. - Why it’s used in your server code:
It allows you to retrieve values for custom arguments passed when starting your server. For example:
nodeserver.js --port=7077
Here,minimist
extractsport=7077
so the server can dynamically bind to the specified port. - Example Usage in Code:
const port = minimist(process.argv.slice(2)).port;
This extracts theport
value (7077
in the example above) from the command-line arguments.
2. morgan
- What it does:
morgan
is a HTTP request logger middleware for Express. It logs information about incoming requests, such as the HTTP method, URL, response status, and response time. - Why it’s used in your server code:
It helps with debugging and monitoring by providing detailed logs of all HTTP requests processed by your server. - Example Logs:
GET /login 200 4.512 ms
POST /login 302 5.123 ms
These logs can help you track activity on your server and identify potential issues. - How it’s added:
app.use(morgan('dev'));
The'dev'
format provides concise colored output in the console.
3. body-parser
- What it does:
body-parser
is a middleware that parses incoming request bodies in a middleware before your handlers (e.g.,req.body
), making the data accessible in JSON or URL-encoded formats. - Why it’s used in your server code:
It processes form data sent via HTTP POST requests, such as login credentials (username
andpassword
) submitted in a form.- The
urlencodedParser
handles URL-encoded data (like data submitted from HTML forms withapplication/x-www-form-urlencoded
).
- The
- Example Usage in Code:
const urlencodedParser = bodyParser.urlencoded({ extended: false }); app.post('/login', urlencodedParser, (req, res) => { console.log(req.body); // Access form fields like req.body.username and req.body.password });
4. express-session
- What it does:
express-session
is middleware that manages sessions in your application. Sessions allow you to store user data (like authentication status) across multiple requests from the same client. - Why it’s used in your server code:
It keeps track of whether a user is logged in. Once a user logs in, their session stores information like their username, and this data persists across subsequent requests. - Key Options in Your Code:
app.use(session({ name: 'cypress-session-cookie', // Custom cookie name secret: 'sekret', // Secret used to sign the session ID cookie resave: false, // Prevents resaving session if no changes are made saveUninitialized: false, // Does not create session until something is stored }));
- A session cookie named
cypress-session-cookie
is sent to the client. - The session data can then be accessed using
req.session
.
- A session cookie named
- Example Usage in Code:
req.session.user = 'jane.lane'; // Set session data
if (req.session.user) { console.log('User is logged in'); }
Cypress script
Check out the whole script here:
/// <reference types="cypress" />
// This recipe is very similar to the 'Logging In - XHR web form'
// except that is uses regular HTML form submission
// instead of using XHR's.
// We are going to test a few things:
// 1. Test unauthorized routes using cy.visit + cy.request
// 2. Test using a regular form submission (old school POSTs)
// 3. Test error states
// 4. Test authenticated session states
// 5. Use cy.request for much faster performance
// 6. Create a custom command
// Be sure to run `npm start` to start the server
// before running the tests below.
describe('Logging In - HTML Web Form', function () {
// we can use these values to log in
const username = 'jane.lane'
const password = 'password123'
context('Unauthorized', function () {
it('is redirected on visit to /dashboard when no session', function () {
// we must have a valid session cookie to be logged
// in else we are redirected to /unauthorized
cy.visit('/dashboard')
cy.get('h3').should(
'contain',
'You are not logged in and cannot access this page'
)
cy.url().should('include', 'unauthorized')
})
it('is redirected using cy.request', function () {
// instead of visiting the page above we can test this by issuing
// a cy.request, checking the status code and redirectedToUrl property.
// See docs for cy.request: https://on.cypress.io/api/request
// the 'redirectedToUrl' property is a special Cypress property under the hood
// that normalizes the url the browser would normally follow during a redirect
cy.request({
url: '/dashboard',
followRedirect: false, // turn off following redirects automatically
}).then((resp) => {
// should have status code 302
expect(resp.status).to.eq(302)
// when we turn off following redirects Cypress will also send us
// a 'redirectedToUrl' property with the fully qualified URL that we
// were redirected to.
expect(resp.redirectedToUrl).to.eq('http://localhost:7077/unauthorized')
})
})
})
context('HTML form submission', function () {
beforeEach(function () {
cy.visit('/login')
})
it('displays errors on login', function () {
// incorrect username on purpose
cy.get('input[name=username]').type('jane.lae')
cy.get('input[name=password]').type('password123{enter}')
// we should have visible errors now
cy.get('p.error')
.should('be.visible')
.and('contain', 'Username and/or password is incorrect')
// and still be on the same URL
cy.url().should('include', '/login')
})
it('redirects to /dashboard on success', function () {
cy.get('input[name=username]').type(username)
cy.get('input[name=password]').type(password)
cy.get('form').submit()
// we should be redirected to /dashboard
cy.url().should('include', '/dashboard')
cy.get('h1').should('contain', 'jane.lane')
// and our cookie should be set to 'cypress-session-cookie'
cy.getCookie('cypress-session-cookie').should('exist')
})
})
context('HTML form submission with cy.request', function () {
it('can bypass the UI and yet still test log in', function () {
// oftentimes once we have a proper e2e test around logging in
// there is NO more reason to actually use our UI to log in users
// doing so wastes is slow because our entire page has to load,
// all associated resources have to load, we have to fill in the
// form, wait for the form submission and redirection process
//
// with cy.request we can bypass this because it automatically gets
// and sets cookies under the hood. This acts exactly as if the requests
// came from the browser
cy.request({
method: 'POST',
url: '/login', // baseUrl will be prepended to this url
form: true, // indicates the body should be form urlencoded and sets Content-Type: application/x-www-form-urlencoded headers
body: {
username,
password,
},
})
// just to prove we have a session
cy.getCookie('cypress-session-cookie').should('exist')
})
})
context('Reusable "login" custom command', function () {
// typically we'd put this in cypress/support/commands.js
// but because this custom command is specific to this example
// we'll keep it here
Cypress.Commands.add('loginByForm', (username, password) => {
Cypress.log({
name: 'loginByForm',
message: `${username} | ${password}`,
})
return cy.request({
method: 'POST',
url: '/login',
form: true,
body: {
username,
password,
},
})
})
beforeEach(function () {
// login before each test
cy.loginByForm(username, password)
})
it('can visit /dashboard', function () {
// after cy.request, the session cookie has been set
// and we can visit a protected page
cy.visit('/dashboard')
cy.get('h1').should('contain', 'jane.lane')
})
it('can visit /users', function () {
// or another protected page
cy.visit('/users')
cy.get('h1').should('contain', 'Users')
})
it('can simply request other authenticated pages', function () {
// instead of visiting each page and waiting for all
// the associated resources to load, we can instead
// just issue a simple HTTP request and make an
// assertion about the response body
cy.request('/admin')
.its('body')
.should('include', '<h1>Admin</h1>')
})
})
})
Overview
This script demonstrates how to test authentication workflows in a web application using Cypress. The tests simulate login scenarios through HTML form submissions, handle unauthorized access, and optimize testing using Cypress commands like cy.visit
and cy.request
. It also showcases reusable custom commands for efficient testing.
Key Features
- Unauthorized Route Testing:
- Validate redirections for unauthenticated users.
- Use
cy.visit
andcy.request
for testing.
- HTML Form Submission Testing:
- Test login success and error states.
- Performance Optimization:
- Leverage
cy.request
to bypass UI and directly interact with APIs for faster tests.
- Leverage
- Custom Command Creation:
- Reuse login functionality through a custom Cypress command.
Script Breakdown
Setup
- Base URL: Ensure the server is running via
npm start
before executing the tests. - Credentials:
const username = 'jane.lane'; const password = 'password123';
Test Cases
1. Unauthorized Access
Tests the behavior when accessing protected routes without a session.
Test: Redirect on Unauthenticated Access
- Scenario: Attempt to visit
/dashboard
without a valid session. - Assertions:
- URL is redirected to
/unauthorized
. - Error message: “You are not logged in and cannot access this page”.
- URL is redirected to
- Code:
cy.visit('/dashboard');
cy.get('h3').should('contain', 'You are not logged in and cannot access this page');
cy.url().should('include', 'unauthorized');
Test: Redirect Validation Using cy.request
- Scenario: Use
cy.request
to verify redirection behavior. - Assertions:
- Status code:
302
. - Redirected URL:
http://localhost:7077/unauthorized
.
- Status code:
- Code:
cy.request({ url: '/dashboard', followRedirect: false, }).then((resp) => { expect(resp.status).to.eq(302); expect(resp.redirectedToUrl).to.eq('http://localhost:7077/unauthorized'); });
2. HTML Form Submission
Simulates login scenarios through standard HTML form submissions.
Test: Error States
- Scenario: Attempt login with incorrect credentials.
- Assertions:
- Error message: “Username and/or password is incorrect”.
- URL remains
/login
.
- Code:
cy.get('input[name=username]').type('jane.lae'); cy.get('input[name=password]').type('password123{enter}'); cy.get('p.error').should('be.visible').and('contain', 'Username and/or password is incorrect'); cy.url().should('include', '/login');
Test: Successful Login
- Scenario: Login with valid credentials.
- Assertions:
- Redirect to
/dashboard
. - Session cookie
cypress-session-cookie
is set.
- Redirect to
- Code:
cy.get('input[name=username]').type(username); cy.get('input[name=password]').type(password); cy.get('form').submit(); cy.url().should('include', '/dashboard'); cy.getCookie('cypress-session-cookie').should('exist');
3. Optimized Login Using cy.request
- Scenario: Use
cy.request
to authenticate without UI interactions. - Advantages:
- Faster test execution.
- Avoids loading unnecessary resources.
- Code:
cy.request({ method: 'POST', url: '/login', form: true, body: { username, password }, });
cy.getCookie('cypress-session-cookie').should('exist');
4. Custom Command for Reusable Login
- Command:
loginByForm
- Defined using
Cypress.Commands.add
. - Sends a POST request to log in.
- Defined using
- Code:
Cypress.Commands.add('loginByForm', (username, password) =>
{ Cypress.log({ name: 'loginByForm', message: `${username} | ${password}` }); return cy.request({ method: 'POST', url: '/login', form: true, body: { username, password }, }); });
Tests Using the Custom Command
- Pre-condition: Log in before each test using
cy.loginByForm(username, password)
.
Test: Visit Protected Pages
/dashboard
:cy.visit('/dashboard');
cy.get('h1').should('contain', 'jane.lane');
/users
:cy.visit('/users');
cy.get('h1').should('contain', 'Users');
Test: Direct API Requests
- Scenario: Use
cy.request
to validate content of/admin
. - Code:
cy.request('/admin').its('body').should('include', '<h1>Admin</h1>');
cypress.config.js
Make sure to update the cypress.config.js file with the following piece of code in order to be able to run the cypress script:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
fixturesFolder: false,
e2e: {
baseUrl: 'http://localhost:7077',
supportFile: false,
},
})