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:
- 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.
- Usually, right after signing up, the user receives an email containing the code they need to enter.
- 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.visit("http://localhost:3000/");
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",
}).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.
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.