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:
- The user clicks "Forgot password", which redirects them to a reset password screen.
- The user provides the email they want to reset the password for, and an email containing a reset link is sent to that address.
- The user clicks the link from the email, taking them to a new page where they can enter their new password.
- 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.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 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.visit("http://localhost:3000/");
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.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
});
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",
}).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.