Password reset with Cypress
Tags: cypress, mailisk, email, testing
Published: 2022-10-06 17:10:42

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](https://github.com/mailisk-app/password-reset-cypress).

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](https://docs.cypress.io/guides/getting-started/installing-cypress). 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](https://mailisk.com) and it's [Cypress plugin](https://docs.mailisk.com/guides/cypress.html). 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](https://docs.mailisk.com/#creating-a-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

```shell
npm install -D cypress-mailisk
```

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

```js
import 'cypress-mailisk'
```

In order to be able to use the API we'll also need to add our [API key](https://docs.mailisk.com/#getting-your-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.

```js
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

```js
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 line

```js
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

```js
test.123456789@mynamespace.mailisk.net
```

This matches the pattern Mailisk uses for emails

```js
<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

```js
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 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](https://docs.mailisk.com/guides/cypress.html#cy-mailisksearchinbox) command for this

```js
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.


![preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ii7jtn5lppz6m06fonp.gif)