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.jsin 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.laneand passwordpassword123to 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:
minimistis 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,minimistextractsport=7077so the server can dynamically bind to the specified port. - Example Usage in Code:
const port = minimist(process.argv.slice(2)).port;
This extracts theportvalue (7077in the example above) from the command-line arguments.
2. morgan
- What it does:
morganis 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 msPOST /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-parseris 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 (usernameandpassword) submitted in a form.- The
urlencodedParserhandles 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-sessionis 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-cookieis 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 dataif (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.visitandcy.requestfor testing.
- HTML Form Submission Testing:
- Test login success and error states.
- Performance Optimization:
- Leverage
cy.requestto 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 startbefore 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
/dashboardwithout 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.requestto 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-cookieis 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.requestto 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.requestto 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,
},
})



