August 3, 2023

Email verification with Cypress

Email verification is standard practice on websites to validate users. It's done during the sign-up phase and ensures that the user says who they say they are (at least the email address). Without email verification, anybody could use any email address.

We can see why this is a test case we would want covered. If it doesn't function correctly, users can sign up using others' email addresses or, worse, prevent legitimate users from signing up.

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

Now, let's explore how email verification functionality usually works:

  1. The user visits the sign-up page, where they create an account. Afterward they are taken to a verify email page.
    • Depending on the flow, the user might have to log in again first.
  2. Usually, right after signing up, the user receives an email containing the code they need to enter.
  3. The user provides an email address and the code they received. If successful, the user is redirected to the dashboard.

We need to solve the issue of retrieving the code from the email, as plain Cypress isn't enough for this step.

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 extracting the code from an email, 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 verification 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 email-verification.cy.js file with the following contents

describe("Test email verification", () => {
  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 verify email screen, as we haven't verified our email yet
    cy.location("pathname").should("eq", "/verify-email");
    // at this point an email with the verification code will be sent by the backend
  });

  it("Should verify email", () => {
    cy.visit("http://localhost:3000/verify-email");

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

  it("Should login as user again", () => {
    // as a sanity check we want to ensure our email is verified, by logging in again
    cy.visit("http://localhost:3000/");
    cy.get("#email").type(testEmailAddress);
    cy.get("#password").type("password");
    cy.get("form").submit();
    // this time we should be redirected to the dashboard since we've verified the email
    cy.location("pathname").should("eq", "/dashboard");
  });
});

This is a basic test which:

  • creates a user
  • tries logging in,
  • verifying the email,
  • and logging in again

You probably noticed the following code

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

We'll need a unique email address for the user. 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 email verification part

it("Should verify email", () => {
  cy.visit("http://localhost:3000/verify-email");

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

In our demo app. After a user signs up, an email with the verification code is sent to their email address. Our task is to read that email, extract the code, and use it on the verify-email page so the email is verified.

Let's use the cy.mailiskSearchInbox command for this

it("Should verify email", () => {
  cy.visit("http://localhost:3000/verify-email");

  let code;
  // mailiskSearchInbox will automatically keep retrying until an email matching the prefix arrives
  // by default it also has a from_timestamp that prevents older emails from being returned by accident
  // find out more here: https://docs.mailisk.com/guides/cypress.html#usage
  cy.mailiskSearchInbox(Cypress.env("MAILISK_NAMESPACE"), {
    to_addr_prefix: testEmailAddress,
    subject_includes: "verify",
    timeout: 1000 * 60,
  }).then((response) => {
    const emails = response.data;
    const email = emails[0];
    // we know that the code is the only number in the email, so we easily filter it out
    code = email.text.match(/\d+/)[0];
    expect(code).to.not.be.undefined;

    // now we enter the code and confirm our email
    cy.get("#email").type(testEmailAddress);
    cy.get("#code").type(code);
    cy.get("form").submit();

    // we should be redirected to the dashboard as proof of a successful verification
    cy.location("pathname").should("eq", "/dashboard");
  });
});

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 "Verify your email", 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 inside this string Your verification code is: {code}. Since the code is the only digits in this string, we can use a simple regex to filter out the number.

Finally, we have our code. We then enter the code (and email since the app requires it), click "Submit" and our user if verified.

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