January 2, 2023
Password reset with Selenium in Node
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.
Selenium Setup
We'll be using the selenium-webdriver
library in Node. You can add this into your project using the following command
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. For an easier setup let's use the chromedriver
npm package which uses one of the latest ChromeDriver versions. It's installed like so
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
- Create a new user, this will make it easier to run the test by itself with no other dependent logic.
- Login as this user, here we'll just confirm that the selected password works.
- 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.
- 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 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
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 library to fetch the password emails. First install it with
npm install mailisk
The test sample above already contains some relevant code, such as importing the library and creating a client
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.
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).
...
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
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
// 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.