Published on

Password reset with Cypress

Every website that has user accounts with passwords has a "Forgot password?" function. Suffice to say that this is a common test case that you usually want to be covered.

If you'd like to follow along or just want to see the final result checkout the repository.

With that out of the way. First, let's take a look at how this password reset functionality usually works:

  1. User clicks "Forgot password", they're taken to a reset password screen
  2. User inputs the email they want to reset the password for, an email is sent
  3. User clicks the link from the email, taking them to a new page where they enter their new password
  4. User can now login with their new password

The hard part here is getting the password reset link from an email. Automating the rest is pretty easy with Cypress.

Cypress

We're using Cypress as the test framework. If this is your first time using Cypress, take a look at their Installation guide. The provided repository for this post already has Cypress setup.

Mailisk

For "getting the password reset link from an email" part, we'll be using Mailisk and it's Cypress plugin. This is an email testing service that offers API endpoints for reading emails.

This lets us create a virtual email server that we can send emails to. The emails look like this <anything>@mynamespace.mailisk.net. Where mynamespace is the name of the namespace.

So any email we send to these addresses can be read using the API, which will allow us to automate the email password reset. But more on that when we get to it.

cypress-mailisk setup

Since we're using Cypress we'll be using the cypress-mailisk plugin. This will give us a simple command to access the received emails.

First install the plugin using

npm install -D cypress-mailisk

After installing the package add the following in your project's cypress/support/e2e.js

import 'cypress-mailisk'

In order to be able to use the API we'll also need to add our API key to cypress.config.js. An additional thing we'll be adding is the MAILISK_NAMESPACE. Doing this will let us easily use a different namespace.

module.exports = defineConfig({
  env: {
    MAILISK_API_KEY: 'YOUR_API_KEY',
    MAILISK_NAMESPACE: 'YOUR_NAMESPACE', // we're also adding this
  },
})

Writing the test

In the app/cypress/e2e folder we'll add a password-reset.cy.js file with the following contents

describe('Test password reset', () => {
  let resetLink
  const testEmailAddress = `test.${new Date().getTime()}@${Cypress.env(
    'MAILISK_NAMESPACE'
  )}.mailisk.net`

  it('Should sign up a new user', () => {
    cy.visit('http://localhost:3000/register')
    cy.get('#email').type(testEmailAddress)
    cy.get('#password').type('password')
    cy.get('form').submit()
    // if the register was successful we should be redirected to the login screen
    cy.location('pathname').should('eq', '/')
  })

  it('Should login as user', () => {
    cy.get('#email').type(testEmailAddress)
    cy.get('#password').type('password')
    cy.get('form').submit()
    // if the login was successful we should be redirected to the dashboard screen
    cy.location('pathname').should('eq', '/dashboard')
  })

  it('Should reset password', () => {
    cy.get('#email').type(testEmailAddress)
    cy.get('form').submit()
    // this will send an email with a reset link to the provided email address

    // TODO: we need to fill this out
  })

  it('Should visit reset link and set new password', () => {
    cy.visit(resetLink)
    cy.get('#new-password').type('newpassword')
    cy.get('form').submit()
    // if the reset was successful we should be redirected to the login screen
    cy.location('pathname').should('eq', '/')
  })

  it('Should login as user with new password', () => {
    cy.get('#email').type(testEmailAddress)
    cy.get('#password').type('newpassword')
    cy.get('form').submit()
    // if the login was successful we should be redirected to the dashboard screen
    cy.location('pathname').should('eq', '/dashboard')
  })
})

This is a basic test which:

  • creates a user
  • tries logging in
  • resetting the password
  • logging in again.

You probably noticed the following line

const testEmailAddress = `test.${new Date().getTime()}@${Cypress.env(
  'MAILISK_NAMESPACE'
)}.mailisk.net`

Since we'll be registering and resetting the password of a user, we need an email for them. This will create a string similar to this

test.123456789@mynamespace.mailisk.net

This matches the pattern Mailisk uses for emails

<anything>@mynamespace.mailisk.net

The only difference is that we added the current time to the email. We're using this as a filtering trick. If we run the same test later, we can be sure there won't be any other emails for this user, which means checking for a new email will be really easy. And since with Mailisk we have unlimited email addresses, there's no worry of using them up.

To improve on this you could change from using the timestamp to using something like a uuid which would make the email address even more unique.

Reading the email

Now let's take a look at the password reset part

it('Should reset password', () => {
  cy.get('#email').type(testEmailAddress)
  cy.get('form').submit()
  // this will send an email with a reset link to the provided email address

  // TODO: we need to fill this out
})

After we enter the email and hit submit and email is sent to that email address. We need to read the email, extract the link and save it in resetLink so we can visit it in the next step.

Let's use the cy.mailiskSearchInbox command for this

it('Should reset password', () => {
  cy.get('#email').type(testEmailAddress)
  cy.get('form').submit()
  // this will send an email with a reset link to the provided email address

  cy.mailiskSearchInbox(Cypress.env('MAILISK_NAMESPACE'), {
    to_addr_prefix: testEmailAddress,
  }).then((response) => {
    const emails = response.data
    const email = emails[0]
    resetLink = email.text.match(/.*\[(http:\/\/localhost:3000\/.*)\].*/)[1]
    expect(resetLink).to.not.be.undefined
  })
})

Let's break down what this does:

mailiskSearchInbox

The cy.mailiskSearchInbox command takes a namespace, options and callback as it's parameters. For the namespace we're using what we defined in the env earlier.

For the options, we pass our email in to_addr_prefix. This way, only emails sent to this email address are returned. A namespace can have unlimited email addresses, so if we don't filter, it would return emails from all addresses in this namespace. Including the ones we're not interested in, like john@mynamespace.mailisk.net.

By default this command also does a few quality of life things for us:

  • It calls the API with the wait flag, this means the call won't timeout until at least one email is received or 5 minutes pass. We can adjust the timeout by passing timeout in the options
  • It uses a default from_timestamp of current timestamp - 5 seconds. This simply means that old emails will be ignored, we wouldn't want the command to return with an old email just because it's still in the inbox.

The callback

In the callback we get the data which represents all the emails found. Since in this scenario only 1 email will be sent, we filter it out. Next, we use some regex to look for this string http://localhost:3000/anythingafterthis.

And et voilà we have our link, we just save this in the global variable resetLink so it can be used in the next test (where it vists the link and resets the password).

And that's it, we've automated the password reset using Cypress and Mailisk.

If we run the E2E spec all of our tests should pass.

result-preview