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.
With that out of the way. First, let's take a look at how this password reset functionality usually works:
- User clicks "Forgot password", they're taken to a reset password screen
- User inputs the email they want to reset the password for, an email is sent
- User clicks the link from the email, taking them to a new page where they enter their new password
- 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. 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 and it's Cypress plugin. 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.
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
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.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
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 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
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 command for this
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 passingtimeout
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.
Top comments (0)