Password reset with Selenium in Node
Tags: email, testing, selenium, node
Published: 2023-01-08 11:58:16

Testing password reset functionality is an important part of ensuring the security and reliability of any web application. In this tutorial, we will learn how to test password reset in Selenium with Node.js.

In order to test this functionality we'll also need some demo app which supports password reset. For ease of use the final code including the test app for this post is available on [GitHub](https://github.com/mailisk-app/password-reset-selenium-node).

## Selenium Setup

We'll be using the `selenium-webdriver` library in Node. You can add this into your project using the following command

```shell
npm install selenium-webdriver
```

We're not setup to use selenium quite yet though, we'll also need to install the relevant _driver_. In this example we'll be using chrome and install the [ChromeDriver](https://sites.google.com/chromium.org/driver/?pli=1). For an easier setup let's use the `chromedriver` npm package which uses one of the latest ChromeDriver versions. It's installed like so

```shell
npm install chromedriver
```

This will make it easier for other developers to checkout our repository since they won't need to do a separate installation step for the driver, doing `npm install` will be enough.

## Writing the test

We'll write a test which focuses on the key aspects of resetting passwords

1. Create a new user, this will make it easier to run the test by itself with no other dependent logic.
2. Login as this user, here we'll just confirm that the selected password works.
3. Reset the password for this user, this will be a two step process. First we'll trigger the password reset email, after it arrives we'll enter our new password and reset it.
4. Login with new password, finally we confirm that the new password is accepted and the password reset works as expected.

As mentioned before, we're using the app available in the [repository](https://github.com/mailisk-app/password-reset-selenium-node) which has both the frontend and backend already prepared.

Let's start by getting the parts not related to emails out of the way. Here are the contents of our `password-reset.test.js`

```js
const { Builder, By } = require("selenium-webdriver");
const { MailiskClient } = require("mailisk");

(async function passwordResetTest() {
  let resetLink;
  const namespace = "mynamespace";
  const testEmailAddress = `test.${new Date().getTime()}@${namespace}.mailisk.net`;

  const mailisk = new MailiskClient({ apiKey: "YOUR_API_KEY" });

  // Create a new Chrome driver
  let driver = await new Builder().forBrowser("chrome").build();

  try {
    // Let's visit the signup page and create a new user
    await driver.get("http://localhost:3000/register");
    await new Promise((r) => setTimeout(r, 500));
    await driver.findElement(By.id("email")).sendKeys(testEmailAddress);
    await driver.findElement(By.id("password")).sendKeys("password");
    await driver.findElement(By.css("form")).submit();
    await new Promise((r) => setTimeout(r, 500));

    // We should have been redirected to the login page, so let's enter our credentials
    await driver.findElement(By.id("email")).sendKeys(testEmailAddress);
    await driver.findElement(By.id("password")).sendKeys("password");
    await driver.findElement(By.css("form")).submit();
    await new Promise((r) => setTimeout(r, 500));

    // Let's send a password reset email
    await driver.get("http://localhost:3000/forgot");
    await new Promise((r) => setTimeout(r, 500));
    await driver.findElement(By.id("email")).sendKeys(testEmailAddress);
    await driver.findElement(By.css("form")).submit();
    // the reset email is sent here!

    // Now we wait for the email to arrive extract the link
    // TODO: we'll need to implement this part

    // We visit the reset link and set the new password
    await driver.get(resetLink);
    await new Promise((r) => setTimeout(r, 500));
    await driver.findElement(By.id("new-password")).sendKeys("newpassword");
    await driver.findElement(By.css("form")).submit();
    await new Promise((r) => setTimeout(r, 500));

    // Let's try logging in again, but this time with the new password
    await driver.get("http://localhost:3000");
    await new Promise((r) => setTimeout(r, 500));
    await driver.findElement(By.id("email")).sendKeys(testEmailAddress);
    await driver.findElement(By.id("password")).sendKeys("newpassword");
    await driver.findElement(By.css("form")).submit();
    await new Promise((r) => setTimeout(r, 500));

    // If we successfully log in, the app will redirect us to the dashboard
    let currentUrl = await driver.getCurrentUrl();
    if (currentUrl !== "http://localhost:3000/dashboard") {
      throw new Error(`Expected url to be 'http://localhost:3000/dashboard' got '${currentUrl}'`);
    }
  } catch (error) {
    console.error(error);
  } finally {
    // Close the browser
    await driver.quit();
  }
})();
```

The provided code above implements most of the key steps in resetting the password. We only need some way to receive the email programmatically and get the reset link.

## Receiving Email

We'll use the [mailisk](https://github.com/mailisk-app/mailisk-node) library to fetch the password emails. First install it with

```shell
npm install mailisk
```

The test sample above already contains some relevant code, such as importing the library and creating a client

```js
const { MailiskClient } = require("mailisk");
...
const mailisk = new MailiskClient({ apiKey: "YOUR_API_KEY" });
```

In order to use the client, you'll also need an api key, you can find yours in the [dashboard](https://docs.mailisk.com/#getting-your-api-key).

Mailisk works based on _namespaces_ you can think of these as a catch-all address which we can access programmatically. For example if we have the namespace `mynamespace` we can find all emails sent to `john@mynamespace.mailisk.net` (you can replace `john` with any valid email address).

```js
...
const namespace = "mynamespace";
const testEmailAddress = `test.${new Date().getTime()}@${namespace}.mailisk.net`;
```

We use this snippet of code to create a unique email address **per test**. Every time the test starts it'll create a new user using a random email address, which will look like this

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

This means we don't have to worry about clutter due to existing emails (e.g. it returning an old email with an invalid reset link).

### searchInbox

In order to read the email we'll fill out the missing code using mailisk's `searchInbox` function

```js
// Now we wait for the email to arrive extract the link
const { data: emails } = await mailisk.searchInbox(namespace, {
  to_addr_prefix: testEmailAddress,
});
const email = emails[0];
resetLink = email.text.match(/.*\[(http:\/\/localhost:3000\/.*)\].*/)[1];
```

The `searchInbox` function takes a namespace and options. We use the `to_addr_prefix` option to filter out emails sent to other addresses (e.g. `john@mynamespace.mailisk.net`). In combination with the email being random this ensures that we're guaranteed to only find the password reset email.

> By default the `searchInbox` function filters out older emails and waits for at least one email to be returned. Which is why we don't need to pool/wait manually for the results.

As a response we'll get an array of emails, but we only care about the one. We'll run some regex to filter out the link and assign it to `resetLink` so it can be used later in the test.

And that's it. We've got an automated test that will reset the password and try logging in again with the new password.