Add support for OTPs and enhance sensitive apis with OTP verification (#191)
One-Time Password (OTP) package added with comprehensive token management, including OTP verification for team account deletion and ownership transfer.
This commit is contained in:
committed by
GitHub
parent
20f7fd2c22
commit
d31f3eb993
@@ -1,14 +1,17 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import { AuthPageObject } from '../authentication/auth.po';
|
||||
import { OtpPo } from '../utils/otp.po';
|
||||
|
||||
export class AccountPageObject {
|
||||
private readonly page: Page;
|
||||
public auth: AuthPageObject;
|
||||
private otp: OtpPo;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.auth = new AuthPageObject(page);
|
||||
this.otp = new OtpPo(page);
|
||||
}
|
||||
|
||||
async setup() {
|
||||
@@ -58,32 +61,16 @@ export class AccountPageObject {
|
||||
await this.page.click('[data-test="account-password-form"] button');
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
await expect(async () => {
|
||||
await this.page.click('[data-test="delete-account-button"]');
|
||||
async deleteAccount(email: string) {
|
||||
// Click the delete account button to open the modal
|
||||
await this.page.click('[data-test="delete-account-button"]');
|
||||
|
||||
await this.page.fill(
|
||||
'[data-test="delete-account-input-field"]',
|
||||
'DELETE',
|
||||
);
|
||||
// Complete the OTP verification process
|
||||
await this.otp.completeOtpVerification(email);
|
||||
|
||||
const click = this.page.click(
|
||||
'[data-test="confirm-delete-account-button"]',
|
||||
);
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
const response = await this.page
|
||||
.waitForResponse((resp) => {
|
||||
return (
|
||||
resp.url().includes('home/settings') &&
|
||||
resp.request().method() === 'POST'
|
||||
);
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.status()).toBe(303);
|
||||
});
|
||||
|
||||
await Promise.all([click, response]);
|
||||
}).toPass();
|
||||
await this.page.click('[data-test="confirm-delete-account-button"]');
|
||||
}
|
||||
|
||||
getProfileName() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Page, expect, test } from '@playwright/test';
|
||||
|
||||
import { AccountPageObject } from './account.po';
|
||||
import {AuthPageObject} from "../authentication/auth.po";
|
||||
|
||||
test.describe('Account Settings', () => {
|
||||
let page: Page;
|
||||
@@ -51,22 +52,22 @@ test.describe('Account Settings', () => {
|
||||
test.describe('Account Deletion', () => {
|
||||
test('user can delete their own account', async ({ page }) => {
|
||||
const account = new AccountPageObject(page);
|
||||
const auth = new AuthPageObject(page);
|
||||
|
||||
await account.setup();
|
||||
const { email } = await account.setup();
|
||||
|
||||
const request = account.deleteAccount();
|
||||
await account.deleteAccount(email);
|
||||
|
||||
const response = page
|
||||
.waitForResponse((resp) => {
|
||||
return (
|
||||
resp.url().includes('home/settings') &&
|
||||
resp.request().method() === 'POST'
|
||||
);
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.status()).toBe(303);
|
||||
});
|
||||
await page.waitForURL('/');
|
||||
|
||||
await Promise.all([request, response]);
|
||||
await page.goto('/auth/sign-in');
|
||||
|
||||
// sign in will now fail
|
||||
await auth.signIn({
|
||||
email,
|
||||
password: 'testingpassword',
|
||||
});
|
||||
|
||||
await expect(page.locator('[data-test="auth-error-message"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ export class AuthPageObject {
|
||||
}
|
||||
|
||||
async signIn(params: { email: string; password: string }) {
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.page.fill('input[name="email"]', params.email);
|
||||
await this.page.fill('input[name="password"]', params.password);
|
||||
@@ -37,7 +37,7 @@ export class AuthPageObject {
|
||||
password: string;
|
||||
repeatPassword: string;
|
||||
}) {
|
||||
await this.page.waitForTimeout(1000);
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await this.page.fill('input[name="email"]', params.email);
|
||||
await this.page.fill('input[name="password"]', params.password);
|
||||
@@ -50,6 +50,7 @@ export class AuthPageObject {
|
||||
email: string,
|
||||
params: {
|
||||
deleteAfter: boolean;
|
||||
subject?: string;
|
||||
} = {
|
||||
deleteAfter: true,
|
||||
},
|
||||
@@ -79,6 +80,10 @@ export class AuthPageObject {
|
||||
});
|
||||
|
||||
await this.visitConfirmEmailLink(email);
|
||||
|
||||
return {
|
||||
email,
|
||||
};
|
||||
}
|
||||
|
||||
async updatePassword(password: string) {
|
||||
|
||||
@@ -51,6 +51,23 @@ test.describe('Auth flow', () => {
|
||||
|
||||
expect(page.url()).toContain('/');
|
||||
});
|
||||
|
||||
test('will sign out using the dropdown', async ({ page }) => {
|
||||
const auth = new AuthPageObject(page);
|
||||
|
||||
await page.goto('/home/settings');
|
||||
|
||||
await auth.signIn({
|
||||
email: 'test@makerkit.dev',
|
||||
password: 'testingpassword',
|
||||
});
|
||||
|
||||
await page.waitForURL('/home/settings');
|
||||
|
||||
await auth.signOut();
|
||||
|
||||
await page.waitForURL('/');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Protected routes', () => {
|
||||
|
||||
@@ -2,35 +2,60 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { AuthPageObject } from './auth.po';
|
||||
|
||||
const email = 'owner@makerkit.dev';
|
||||
const newPassword = (Math.random() * 10000).toString();
|
||||
|
||||
test.describe('Password Reset Flow', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test('will reset the password and sign in with new one', async ({ page }) => {
|
||||
const auth = new AuthPageObject(page);
|
||||
|
||||
await page.goto('/auth/password-reset');
|
||||
let email = '';
|
||||
|
||||
await page.fill('[name="email"]', email);
|
||||
await page.click('[type="submit"]');
|
||||
await expect(async () => {
|
||||
email = `test-${Math.random() * 10000}@makerkit.dev`;
|
||||
|
||||
await auth.visitConfirmEmailLink(email);
|
||||
await page.goto('/auth/sign-up');
|
||||
|
||||
await page.waitForURL('/update-password');
|
||||
await auth.signUp({
|
||||
email,
|
||||
password: 'password',
|
||||
repeatPassword: 'password',
|
||||
});
|
||||
|
||||
await auth.updatePassword(newPassword);
|
||||
await auth.visitConfirmEmailLink(email, {
|
||||
deleteAfter: true,
|
||||
subject: 'Confirm your email',
|
||||
});
|
||||
|
||||
await page
|
||||
.locator('a', {
|
||||
hasText: 'Back to Home Page',
|
||||
})
|
||||
.click();
|
||||
await page.context().clearCookies();
|
||||
await page.reload();
|
||||
|
||||
await page.waitForURL('/home');
|
||||
await page.goto('/auth/password-reset');
|
||||
|
||||
await auth.signOut();
|
||||
await page.fill('[name="email"]', email);
|
||||
await page.click('[type="submit"]');
|
||||
|
||||
await auth.visitConfirmEmailLink(email, {
|
||||
deleteAfter: true,
|
||||
subject: 'Reset your password',
|
||||
});
|
||||
|
||||
await page.waitForURL('/update-password', {
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
await auth.updatePassword(newPassword);
|
||||
|
||||
await page
|
||||
.locator('a', {
|
||||
hasText: 'Back to Home Page',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.waitForURL('/home');
|
||||
}).toPass();
|
||||
|
||||
await page.context().clearCookies();
|
||||
await page.reload();
|
||||
|
||||
await page
|
||||
.locator('a', {
|
||||
@@ -43,6 +68,8 @@ test.describe('Password Reset Flow', () => {
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
await page.waitForURL('/home');
|
||||
await page.waitForURL('/home', {
|
||||
timeout: 2000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export class InvitationsPageObject {
|
||||
})
|
||||
.click();
|
||||
|
||||
return expect(this.page.url()).toContain('members');
|
||||
await this.page.waitForURL('**/home/*/members');
|
||||
}).toPass()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import { AuthPageObject } from '../authentication/auth.po';
|
||||
import { OtpPo } from '../utils/otp.po';
|
||||
|
||||
export class TeamAccountsPageObject {
|
||||
private readonly page: Page;
|
||||
public auth: AuthPageObject;
|
||||
public otp: OtpPo;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.auth = new AuthPageObject(page);
|
||||
this.otp = new OtpPo(page);
|
||||
}
|
||||
|
||||
async setup(params = this.createTeamName()) {
|
||||
await this.auth.signUpFlow('/home');
|
||||
|
||||
const { email } = await this.auth.signUpFlow('/home');
|
||||
|
||||
await this.createTeam(params);
|
||||
|
||||
return { email, teamName: params.teamName, slug: params.slug };
|
||||
}
|
||||
|
||||
getTeamFromSelector(teamName: string) {
|
||||
@@ -39,6 +44,18 @@ export class TeamAccountsPageObject {
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
goToMembers() {
|
||||
return expect(async () => {
|
||||
await this.page
|
||||
.locator('a', {
|
||||
hasText: 'Members',
|
||||
})
|
||||
.click();
|
||||
|
||||
await this.page.waitForURL('**/home/*/members');
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
goToBilling() {
|
||||
return expect(async () => {
|
||||
await this.page
|
||||
@@ -94,18 +111,11 @@ export class TeamAccountsPageObject {
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
async deleteAccount(teamName: string) {
|
||||
async deleteAccount(email: string) {
|
||||
await expect(async () => {
|
||||
await this.page.click('[data-test="delete-team-trigger"]');
|
||||
|
||||
await expect(
|
||||
this.page.locator('[data-test="delete-team-form-confirm-input"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await this.page.fill(
|
||||
'[data-test="delete-team-form-confirm-input"]',
|
||||
teamName,
|
||||
);
|
||||
await this.otp.completeOtpVerification(email);
|
||||
|
||||
const click = this.page.click(
|
||||
'[data-test="delete-team-form-confirm-button"]',
|
||||
@@ -117,6 +127,51 @@ export class TeamAccountsPageObject {
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
async updateMemberRole(memberEmail: string, newRole: string) {
|
||||
await expect(async () => {
|
||||
// Find the member row and click the actions button
|
||||
const memberRow = this.page.getByRole('row', { name: memberEmail });
|
||||
await memberRow.getByRole('button').click();
|
||||
|
||||
// Click the update role option in the dropdown menu
|
||||
await this.page.getByText('Update Role').click();
|
||||
|
||||
// Select the new role
|
||||
await this.page.click('[data-test="role-selector-trigger"]');
|
||||
await this.page.click(`[data-test="role-option-${newRole}"]`);
|
||||
|
||||
// Click the confirm button
|
||||
const click = this.page.click('[data-test="confirm-update-member-role"]');
|
||||
|
||||
// Wait for the update to complete and page to reload
|
||||
const response = this.page.waitForURL('**/home/*/members');
|
||||
|
||||
return Promise.all([click, response]);
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
async transferOwnership(memberEmail: string, ownerEmail: string) {
|
||||
await expect(async () => {
|
||||
// Find the member row and click the actions button
|
||||
const memberRow = this.page.getByRole('row', { name: memberEmail });
|
||||
await memberRow.getByRole('button').click();
|
||||
|
||||
// Click the transfer ownership option in the dropdown menu
|
||||
await this.page.getByText('Transfer Ownership').click();
|
||||
|
||||
// Complete OTP verification
|
||||
await this.otp.completeOtpVerification(ownerEmail);
|
||||
|
||||
// Click the confirm button
|
||||
const click = this.page.click('[data-test="confirm-transfer-ownership-button"]');
|
||||
|
||||
// Wait for the transfer to complete and page to reload
|
||||
const response = this.page.waitForURL('**/home/*/members');
|
||||
|
||||
return Promise.all([click, response]);
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
createTeamName() {
|
||||
const random = (Math.random() * 100000000).toFixed(0);
|
||||
|
||||
|
||||
@@ -1,7 +1,74 @@
|
||||
import { Page, expect, test } from '@playwright/test';
|
||||
|
||||
import { InvitationsPageObject } from '../invitations/invitations.po';
|
||||
import { TeamAccountsPageObject } from './team-accounts.po';
|
||||
|
||||
// Helper function to set up a team with a member
|
||||
async function setupTeamWithMember(page: Page, memberRole = 'member') {
|
||||
// Setup invitations page object
|
||||
const invitations = new InvitationsPageObject(page);
|
||||
const teamAccounts = invitations.teamAccounts;
|
||||
|
||||
// Setup team with owner
|
||||
const { email: ownerEmail, slug } = await invitations.setup();
|
||||
|
||||
// Navigate to members page
|
||||
await invitations.navigateToMembers();
|
||||
|
||||
// Create a new member email and invite them with the specified role
|
||||
const memberEmail = invitations.auth.createRandomEmail();
|
||||
|
||||
const invites = [
|
||||
{
|
||||
email: memberEmail,
|
||||
role: memberRole,
|
||||
},
|
||||
];
|
||||
|
||||
await invitations.openInviteForm();
|
||||
await invitations.inviteMembers(invites);
|
||||
|
||||
// Verify the invitation was sent
|
||||
await expect(invitations.getInvitations()).toHaveCount(1);
|
||||
|
||||
// Sign out the current user
|
||||
await page.context().clearCookies();
|
||||
|
||||
// Sign up with the new member email and accept the invitation
|
||||
await invitations.auth.visitConfirmEmailLink(memberEmail);
|
||||
|
||||
await invitations.auth.signUp({
|
||||
email: memberEmail,
|
||||
password: 'password',
|
||||
repeatPassword: 'password',
|
||||
});
|
||||
|
||||
await invitations.auth.visitConfirmEmailLink(memberEmail);
|
||||
|
||||
await invitations.acceptInvitation();
|
||||
|
||||
await invitations.teamAccounts.openAccountsSelector();
|
||||
|
||||
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
|
||||
|
||||
// Sign out and sign back in as the original owner
|
||||
await page.context().clearCookies();
|
||||
|
||||
await page.goto('/auth/sign-in');
|
||||
|
||||
await invitations.auth.signIn({
|
||||
email: ownerEmail,
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
await page.waitForURL('/home');
|
||||
|
||||
// Navigate to the team members page
|
||||
await page.goto(`/home/${slug}/members`);
|
||||
|
||||
return { invitations, teamAccounts, ownerEmail, memberEmail, slug };
|
||||
}
|
||||
|
||||
test.describe('Team Accounts', () => {
|
||||
let page: Page;
|
||||
let teamAccounts: TeamAccountsPageObject;
|
||||
@@ -31,15 +98,15 @@ test.describe('Team Accounts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Account Deletion', () => {
|
||||
test.describe('Team Account Deletion', () => {
|
||||
test('user can delete their team account', async ({ page }) => {
|
||||
const teamAccounts = new TeamAccountsPageObject(page);
|
||||
const params = teamAccounts.createTeamName();
|
||||
|
||||
await teamAccounts.setup(params);
|
||||
const { email } = await teamAccounts.setup(params);
|
||||
await teamAccounts.goToSettings();
|
||||
|
||||
await teamAccounts.deleteAccount(params.teamName);
|
||||
await teamAccounts.deleteAccount(email);
|
||||
await teamAccounts.openAccountsSelector();
|
||||
|
||||
await expect(
|
||||
@@ -47,3 +114,62 @@ test.describe('Account Deletion', () => {
|
||||
).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Team Member Role Management', () => {
|
||||
test("owner can update a team member's role", async ({ page }) => {
|
||||
// Setup team with a regular member
|
||||
const { teamAccounts, memberEmail } = await setupTeamWithMember(page);
|
||||
|
||||
// Get the current role badge text
|
||||
const memberRow = page.getByRole('row', { name: memberEmail });
|
||||
|
||||
const initialRoleBadge = memberRow.locator(
|
||||
'[data-test="member-role-badge"]',
|
||||
);
|
||||
|
||||
await expect(initialRoleBadge).toHaveText('Member');
|
||||
|
||||
// Update the member's role to admin
|
||||
await teamAccounts.updateMemberRole(memberEmail, 'owner');
|
||||
|
||||
// Wait for the page to fully load after the update
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify the role was updated successfully
|
||||
const updatedRoleBadge = page
|
||||
.getByRole('row', { name: memberEmail })
|
||||
.locator('[data-test="member-role-badge"]');
|
||||
await expect(updatedRoleBadge).toHaveText('Owner');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Team Ownership Transfer', () => {
|
||||
test('owner can transfer ownership to another team member', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Setup team with an owner member (required for ownership transfer)
|
||||
const { teamAccounts, ownerEmail, memberEmail } = await setupTeamWithMember(
|
||||
page,
|
||||
'owner',
|
||||
);
|
||||
|
||||
// Transfer ownership to the member
|
||||
await teamAccounts.transferOwnership(memberEmail, ownerEmail);
|
||||
|
||||
// Wait for the page to fully load after the transfer
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify the transfer was successful by checking if the primary owner badge
|
||||
// is now on the new owner's row
|
||||
const memberRow = page.getByRole('row', { name: memberEmail });
|
||||
|
||||
// Check for the primary owner badge on the member's row
|
||||
await expect(memberRow.locator('text=Primary Owner')).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// The original owner should no longer have the primary owner badge
|
||||
const ownerRow = page.getByRole('row', { name: ownerEmail.split('@')[0] });
|
||||
await expect(ownerRow.locator('text=Primary Owner')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export class Mailbox {
|
||||
email: string,
|
||||
params: {
|
||||
deleteAfter: boolean;
|
||||
subject?: string;
|
||||
},
|
||||
) {
|
||||
const mailbox = email.split('@')[0];
|
||||
@@ -18,13 +19,17 @@ export class Mailbox {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
|
||||
const json = await this.getInviteEmail(mailbox, params);
|
||||
const json = await this.getEmail(mailbox, params);
|
||||
|
||||
if (!json?.body) {
|
||||
throw new Error('Email body was not found');
|
||||
}
|
||||
|
||||
console.log('Email found');
|
||||
console.log(`Email found for ${email}`, {
|
||||
id: json.id,
|
||||
subject: json.subject,
|
||||
date: json.date,
|
||||
});
|
||||
|
||||
const html = (json.body as { html: string }).html;
|
||||
const el = parse(html);
|
||||
@@ -40,10 +45,49 @@ export class Mailbox {
|
||||
return this.page.goto(linkHref);
|
||||
}
|
||||
|
||||
async getInviteEmail(
|
||||
/**
|
||||
* Retrieves an OTP code from an email
|
||||
* @param email The email address to check for the OTP
|
||||
* @param deleteAfter Whether to delete the email after retrieving the OTP
|
||||
* @returns The OTP code
|
||||
*/
|
||||
async getOtpFromEmail(email: string, deleteAfter: boolean = true) {
|
||||
const mailbox = email.split('@')[0];
|
||||
|
||||
console.log(`Retrieving OTP from mailbox ${email} ...`);
|
||||
|
||||
if (!mailbox) {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
|
||||
const json = await this.getEmail(mailbox, {
|
||||
deleteAfter,
|
||||
subject: `One-time password for Makerkit`,
|
||||
});
|
||||
|
||||
if (!json?.body) {
|
||||
throw new Error('Email body was not found');
|
||||
}
|
||||
|
||||
const html = (json.body as { html: string }).html;
|
||||
|
||||
const text = html.match(
|
||||
new RegExp(`Your one-time password is: (\\d{6})`),
|
||||
)?.[1];
|
||||
|
||||
if (text) {
|
||||
console.log(`OTP code found in text: ${text}`);
|
||||
return text;
|
||||
}
|
||||
|
||||
throw new Error('Could not find OTP code in email');
|
||||
}
|
||||
|
||||
async getEmail(
|
||||
mailbox: string,
|
||||
params: {
|
||||
deleteAfter: boolean;
|
||||
subject?: string;
|
||||
},
|
||||
) {
|
||||
const url = `http://127.0.0.1:54324/api/v1/mailbox/${mailbox}`;
|
||||
@@ -54,13 +98,34 @@ export class Mailbox {
|
||||
throw new Error(`Failed to fetch emails: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const json = (await response.json()) as Array<{ id: string }>;
|
||||
const json = (await response.json()) as Array<{
|
||||
id: string;
|
||||
subject: string;
|
||||
}>;
|
||||
|
||||
if (!json || !json.length) {
|
||||
console.log(`No emails found for mailbox ${mailbox}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = json[0]?.id;
|
||||
const message = params.subject
|
||||
? (() => {
|
||||
const filtered = json.filter(
|
||||
(item) => item.subject === params.subject,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Found ${filtered.length} emails with subject ${params.subject}`,
|
||||
);
|
||||
|
||||
return filtered[filtered.length - 1];
|
||||
})()
|
||||
: json[0];
|
||||
|
||||
console.log(`Message: ${JSON.stringify(message)}`);
|
||||
|
||||
const messageId = message?.id;
|
||||
const messageUrl = `${url}/${messageId}`;
|
||||
|
||||
const messageResponse = await fetch(messageUrl);
|
||||
|
||||
63
apps/e2e/tests/utils/otp.po.ts
Normal file
63
apps/e2e/tests/utils/otp.po.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import { Mailbox } from './mailbox';
|
||||
|
||||
export class OtpPo {
|
||||
private readonly page: Page;
|
||||
private readonly mailbox: Mailbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.mailbox = new Mailbox(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the OTP verification process
|
||||
* @param email The email address to send the OTP to
|
||||
*/
|
||||
async completeOtpVerification(email: string) {
|
||||
// Click the "Send Verification Code" button
|
||||
await this.page.click('[data-test="otp-send-verification-button"]');
|
||||
|
||||
// wait for the OTP to be sent
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
await expect(async () => {
|
||||
// Get the OTP code from the email
|
||||
const otpCode = await this.getOtpCodeFromEmail(email);
|
||||
|
||||
expect(otpCode).not.toBeNull();
|
||||
|
||||
// Enter the OTP code
|
||||
await this.enterOtpCode(otpCode);
|
||||
}).toPass();
|
||||
|
||||
// Click the "Verify Code" button
|
||||
await this.page.click('[data-test="otp-verify-button"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the OTP code from an email
|
||||
* @param email The email address to check for the OTP
|
||||
* @returns The OTP code
|
||||
*/
|
||||
async getOtpCodeFromEmail(email: string) {
|
||||
// Get the OTP from the email
|
||||
const otpCode = await this.mailbox.getOtpFromEmail(email);
|
||||
|
||||
if (!otpCode) {
|
||||
throw new Error('Failed to retrieve OTP code from email');
|
||||
}
|
||||
|
||||
return otpCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters the OTP code into the input fields
|
||||
* @param otpCode The 6-digit OTP code
|
||||
*/
|
||||
async enterOtpCode(otpCode: string) {
|
||||
console.log(`Entering OTP code: ${otpCode}`);
|
||||
await this.page.fill('[data-input-otp]', otpCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user