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,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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user