Passwordless authentication is becoming more and more popular. It’s an easy way to have authentication functionality. The main advantage of having magic links is better security. One password to remember less — avoid reuse passwords by users. Additionally, you make sure the user confirms their account with email.
The pain point of using email authentication is testing. Not anymore!
Table of contest
This post is inspired by work on my project https://happyreact.com/. Add a feedback widget to your product documentation. Built a better product, avoid user churn and drive more sales!
Let’s start
We will use NextAuth.js bootstrapped from this example with an email provider turned on. It will be a great and easy-to-use playground for our testing.
As stated in the title, our testing framework will be Playwright. I’m using it in my projects and it has excellent developer experience.
Complementary packages:
smtp-tester for starting the SMTP local server
cheerio for finding the link in the email’s HTML
nodemailer for sending emails
I configured the email provider and added Playwright. Nothing more than following standard installation from Playwright and NextAuth email adapter setup. Follow the next auth email provider guide on how to set it up.
TL;DR;
Install these packages:
yarn add smtp-tester cheerio --dev
Import smtp-tester and cheerio:
import { test } from '@playwright/test';
import smtpTester from 'smtp-tester';
import { load as cheerioLoad } from 'cheerio';
Your test should look like this:
test.describe('authenticated', () => {
let mailServer;
test.beforeAll(() => {
mailServer = smtpTester.init(4025);
});
test.afterAll(() => {
mailServer.stop();
});
test('login to app', async ({ page, context }) => {
await page.goto('/login');
// your login page test logic
await page
.locator('input[name="email"]')
.type('test@example.com', { delay: 100 });
await page.locator('text=Request magic link').click({ delay: 100 });
await page.waitForSelector(
`text=Check your e-mail inbox for further instructions`
);
let emailLink = null;
try {
const { email } = await mailServer.captureOne('test@example.com', {
wait: 1000
});
const $ = cheerioLoad(email.html);
emailLink = $('#magic-link').attr('href');
} catch (cause) {
console.error(
'No message delivered to test@example.com in 1 second.',
cause
);
}
expect(emailLink).not.toBeFalsy();
const res = await page.goto(emailLink);
expect(res?.url()?.endsWith('/dashboard')).toBeTruthy();
await context.storageState({
path: path.resolve('tests', 'test-results', 'state.json')
});
}
});
Sent emails through our created server:
// This check will work when you pass NODE_ENV with 'test' value
// when running your e2e tests
const transport = process.env.NODE_ENV === 'test'
? nodemailer.createTransport({ port: 4025 })
: nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
⚠️ Make sure to send emails through this SMTP server ONLY in testing!
Setting up an email provider
At this point, I assume you have set up an SMTP provider and you are familiar with that matter. When you need a guide, please see “How to set up an SMTP provider for testing” on my blog.
Let’s make our authenticated test. Create authenticated.spec.ts a file in our tests directory with an empty describe block. We want to use beforeAll and afterAll hooks.
// authenticated.spec.ts
import { test } from '@playwright/test';
test.describe('authenticated', () => {
test.beforeAll(() => {});
test.afterAll(() => {});
})
Next, install smtp-tester package and import it into our created test. Run the following command:
yarn add smtp-tester cheerio --dev
and add a start/stop SMTP server
import { test } from '@playwright/test';
import smtpTester from 'smtp-tester';
import { load as cheerioLoad } from 'cheerio';
test.describe('authenticated', () => {
let mailServer: any;
test.beforeAll(() => {
mailServer = smtpTester.init(4025);
});
test.afterAll(() => {
mailServer.stop();
});
});
What is happening here?
Before all tests, we are starting the SMTP server
After all tests, we are stopping this service
The important part is that we are creating our SMTP server on port 4025. We will use this port later to send emails.
Testing login form
Now let’s write some test logic. You should adjust texts to what you show in your application after certain actions. HappyReact texts are as follows.
// authenticated.spec.ts
test('login to app', async ({ page, context }) => {
await page.goto('/login');
// your login page test logic
await page
.locator('input[name="email"]')
.type('test@example.com', { delay: 100 });
await page.locator('text=Request magic link').click({ delay: 100 });
await page.waitForSelector(
`text=Check your e-mail inbox for further instructions`
);
let emailLink = null as any;
try {
const { email } = await mailServer.captureOne('test@example.com', {
wait: 1000
});
const $ = cheerioLoad(email.html);
emailLink = $('#magic-link').attr('href');
} catch (cause) {
console.error(
'No message delivered to test@example.com in 1 second.',
cause
);
}
expect(emailLink).not.toBeFalsy();
const res = await page.goto(emailLink);
expect(res?.url()?.endsWith('/dashboard')).toBeTruthy();
await context.storageState({
path: path.resolve('tests', 'test-results', 'state.json')
});
}
What is happening here?
Going to the login page and filling out our email address
Capturing email sent to our email address
Loading HTML from the email into cheerio and finding a login link
Checking if the link is present and visiting it
Saving auth cookies to use them later in tests (optional)
💡 It’s a good idea to add id to link with a URL in the email template. This will let you find it much easier.
Remember about our SMTP server is running on port 4025? We need to instruct nodemailer to send emails using this server instead standard one. It’s the only way to capture email in tests and prevent it from being sent to a real e-mail address. Be sure that you are using it only in testing.
// This check will work when you pass NODE_ENV with 'test' value
// when running your e2e tests
const transport = process.env.NODE_ENV === 'test'
? nodemailer.createTransport({ port: 4025 })
: nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
⚠️ Make sure to send emails through this SMTP server ONLY in testing!
Wrapping up
Testing sending emails was always a pain. This technique can be extended. Alongside authentication, you can test other functionalities like invoices or team invites. Results with the better application.
Top comments (0)