October 4, 2022

Password reset with Cypress

Every website that has user accounts with passwords typically includes a "Forgot password?" function, which is a common test case that needs to be covered.

If you want to follow along or view the final result, you can check out the repository.

Now, let's explore how the password reset functionality usually works:

  1. The user clicks "Forgot password", which redirects them to a reset password screen.
  2. The user provides the email they want to reset the password for, and an email containing a reset link is sent to that address.
  3. The user clicks the link from the email, taking them to a new page where they can enter their new password.
  4. The 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 testing framework. If this is your first time using Cypress, take a look at their Installation guide. The provided repository in 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. Mailisk is an email testing service offering API endpoints for reading emails.

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

Sending emails to these addresses allows us to read them using the API, enabling us to automate the email password reset process. We'll look at this part again later.

cypress-mailisk setup

Since we're using Cypress as our testing framework, we'll also 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.visit("http://localhost:3000/forgot");
    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 code

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 mechanism. If we run the same test later, we can be sure there won't be any other emails for this user. This way 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.

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 entering the email and clicking the submit button, an email is sent to the provided email address. Our task is to read the email, extract the link, and store it in the variable resetLink so that we can visit it in the subsequent step.

Let's use the cy.mailiskSearchInbox command for this

it("Should reset password", () => {
  cy.visit("http://localhost:3000/forgot");
  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,
    subject_includes: "reset",
    timeout: 1000 * 60,
  }).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. We're using the namespace that 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.

Since a namespace can have unlimited email addresses, if we don't filter, it would return emails from all addresses in this namespace. Including the ones we're not interested in, such as john@mynamespace.mailisk.net.

subject_includes filters our email such that it must include the string in the subject (case-insensitive). Since we know that the subject will be "Reset password code", we can also filter by it. This is essential for multi-step tests that might first register an account (register email) then verify it (verify email). If we didn't filter by subject we could get the incorrect email.

Finally we use timeout, otherwise we'd wait forever for an email if something went wrong.

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

Ready to start testing emails?
Create a free account.

Get started