diff --git a/apps/meteor/client/views/account/AccountSidebar.tsx b/apps/meteor/client/views/account/AccountSidebar.tsx index a651061f7a41f..aaabf34132b18 100644 --- a/apps/meteor/client/views/account/AccountSidebar.tsx +++ b/apps/meteor/client/views/account/AccountSidebar.tsx @@ -17,7 +17,7 @@ const AccountSidebar = () => { // TODO: uplift this provider return ( - + diff --git a/apps/meteor/tests/e2e/access-security-page.spec.ts b/apps/meteor/tests/e2e/access-security-page.spec.ts deleted file mode 100644 index 43676b5e73283..0000000000000 --- a/apps/meteor/tests/e2e/access-security-page.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Users } from './fixtures/userStates'; -import { AccountProfile } from './page-objects'; -import { setSettingValueById } from './utils/setSettingValueById'; -import { test, expect } from './utils/test'; - -test.use({ storageState: Users.admin.state }); - -test.describe.serial('access-security-page', () => { - let poAccountProfile: AccountProfile; - - test.beforeAll(async ({ api }) => { - await Promise.all([ - setSettingValueById(api, 'Accounts_AllowPasswordChange', false), - setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false), - setSettingValueById(api, 'E2E_Enable', false), - ]); - }); - - test.beforeEach(async ({ page }) => { - poAccountProfile = new AccountProfile(page); - await page.goto('/account/security'); - await page.waitForSelector('#main-content'); - }); - - test.afterAll(async ({ api }) => - Promise.all([ - setSettingValueById(api, 'Accounts_AllowPasswordChange', true), - setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', true), - setSettingValueById(api, 'E2E_Enable', false), - ]), - ); - - test('security tab is invisible when password change, 2FA and E2E are disabled', async ({ page }) => { - const securityTab = poAccountProfile.sidenav.linkSecurity; - await expect(securityTab).not.toBeVisible(); - const mainContent = page.locator('#main-content').getByText('You are not authorized to view this page.').first(); - await expect(mainContent).toBeVisible(); - }); - - test.describe.serial('can access account security sections', () => { - test.beforeAll(async ({ api }) => { - await Promise.all([ - setSettingValueById(api, 'Accounts_AllowPasswordChange', true), - setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false), - setSettingValueById(api, 'E2E_Enable', false), - ]); - }); - - test.beforeEach(async () => { - await poAccountProfile.securityHeader.waitFor({ state: 'visible' }); - }); - - test('security page is visible when password change is enabled but 2FA and E2E are disabled', async () => { - const securityTab = poAccountProfile.sidenav.linkSecurity; - - await expect(securityTab).toBeVisible(); - }); - - test('can access password change when enabled but 2FA and E2E are disabled', async () => { - await expect(poAccountProfile.securityPasswordSection).toBeVisible(); - }); - - test('can access 2FA setting when enabled but password change and E2E are disabled', async ({ api }) => { - await Promise.all([ - setSettingValueById(api, 'Accounts_AllowPasswordChange', false), - setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', true), - setSettingValueById(api, 'E2E_Enable', false), - ]); - await expect(poAccountProfile.security2FASection).toBeVisible(); - }); - - test('can access E2E setting when enabled but password change and 2FA are disabled', async ({ api }) => { - await Promise.all([ - setSettingValueById(api, 'Accounts_AllowPasswordChange', false), - setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false), - setSettingValueById(api, 'E2E_Enable', true), - ]); - await expect(poAccountProfile.securityE2EEncryptionSection).toBeVisible(); - }); - }); -}); diff --git a/apps/meteor/tests/e2e/account-profile.spec.ts b/apps/meteor/tests/e2e/account-profile.spec.ts index a17de8468a1ed..6672db4668928 100644 --- a/apps/meteor/tests/e2e/account-profile.spec.ts +++ b/apps/meteor/tests/e2e/account-profile.spec.ts @@ -68,31 +68,6 @@ test.describe.serial('settings-account-profile', () => { }); }); - test.describe('Security', () => { - test.beforeEach(async ({ page }) => { - await page.goto('account/security'); - await page.waitForSelector('#main-content'); - }); - - test('should not have any accessibility violations', async ({ page, makeAxeBuilder }) => { - await page.goto('/account/security'); - - const results = await makeAxeBuilder().analyze(); - expect(results.violations).toEqual([]); - }); - - test('should disable and enable email 2FA', async () => { - await poAccountProfile.security2FASection.click(); - await expect(poAccountProfile.email2FASwitch).toBeVisible(); - await poAccountProfile.email2FASwitch.click(); - await poHomeChannel.toastMessage.waitForDisplay(); - await poHomeChannel.toastMessage.dismissToast(); - - await poAccountProfile.email2FASwitch.click(); - await poHomeChannel.toastMessage.waitForDisplay(); - }); - }); - test('Personal Access Tokens', async ({ page }) => { const response = page.waitForResponse('**/api/v1/users.getPersonalAccessTokens'); await page.goto('/account/tokens'); diff --git a/apps/meteor/tests/e2e/account-security.spec.ts b/apps/meteor/tests/e2e/account-security.spec.ts new file mode 100644 index 0000000000000..1fb41326cf67a --- /dev/null +++ b/apps/meteor/tests/e2e/account-security.spec.ts @@ -0,0 +1,108 @@ +import { faker } from '@faker-js/faker'; + +import { ADMIN_CREDENTIALS } from './config/constants'; +import { Users } from './fixtures/userStates'; +import { AccountSecurity } from './page-objects'; +import { setSettingValueById, updateOwnUserPassword } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +const RANDOM_PASSWORD = faker.string.alphanumeric(10); + +test.describe.serial('account-security', () => { + let poAccountSecurity: AccountSecurity; + + test.beforeEach(async ({ page }) => { + poAccountSecurity = new AccountSecurity(page); + await page.goto('/account/security'); + await page.waitForSelector('#main-content'); + }); + + test.afterAll(async ({ api }) => + Promise.all([ + setSettingValueById(api, 'Accounts_AllowPasswordChange', true), + setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', true), + setSettingValueById(api, 'E2E_Enable', false), + ]), + ); + + test('should disable and enable email 2FA', async () => { + await poAccountSecurity.security2FASection.click(); + await expect(poAccountSecurity.email2FASwitch).toBeVisible(); + await poAccountSecurity.email2FASwitch.click(); + await poAccountSecurity.toastMessage.waitForDisplay(); + await poAccountSecurity.toastMessage.dismissToast(); + + await poAccountSecurity.email2FASwitch.click(); + await poAccountSecurity.toastMessage.waitForDisplay(); + }); + + // FIXME: This test should pass as soon as we provide the fix + test.skip('should be able to change password', async ({ api }) => { + await test.step('change password', async () => { + await poAccountSecurity.changePassword(RANDOM_PASSWORD, RANDOM_PASSWORD, ADMIN_CREDENTIALS.password); + await expect(poAccountSecurity.inputNewPassword).toHaveValue(''); + }); + + await test.step('change back to the original password', async () => { + expect( + (await updateOwnUserPassword(api, { newPassword: ADMIN_CREDENTIALS.password, currentPassword: RANDOM_PASSWORD })).status(), + ).toBe(200); + }); + }); + + test.describe('settings disabled', () => { + test.beforeAll(async ({ api }) => { + await Promise.all([ + setSettingValueById(api, 'Accounts_AllowPasswordChange', false), + setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false), + setSettingValueById(api, 'E2E_Enable', false), + ]); + }); + + test('security tab is invisible when password change, 2FA and E2E are disabled', async ({ page }) => { + const securityTab = poAccountSecurity.sidebar.linkSecurity; + await expect(securityTab).not.toBeVisible(); + const mainContent = page.locator('#main-content').getByText('You are not authorized to view this page.').first(); + await expect(mainContent).toBeVisible(); + }); + }); + + test.describe('account security sections', () => { + test.beforeAll(async ({ api }) => { + await Promise.all([ + setSettingValueById(api, 'Accounts_AllowPasswordChange', true), + setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false), + setSettingValueById(api, 'E2E_Enable', false), + ]); + }); + + test.beforeEach(async () => { + await poAccountSecurity.securityHeader.waitFor({ state: 'visible' }); + }); + + test('should display security tab and section when password change is enabled but 2FA and E2E are disabled', async () => { + await expect(poAccountSecurity.sidebar.linkSecurity).toBeVisible(); + await expect(poAccountSecurity.securityPasswordSection).toBeVisible(); + }); + + test('can access 2FA setting when enabled but password change and E2E are disabled', async ({ api }) => { + await Promise.all([ + setSettingValueById(api, 'Accounts_AllowPasswordChange', false), + setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', true), + setSettingValueById(api, 'E2E_Enable', false), + ]); + await expect(poAccountSecurity.security2FASection).toBeVisible(); + }); + + test('can access E2E setting when enabled but password change and 2FA are disabled', async ({ api }) => { + await Promise.all([ + setSettingValueById(api, 'Accounts_AllowPasswordChange', false), + setSettingValueById(api, 'Accounts_TwoFactorAuthentication_Enabled', false), + setSettingValueById(api, 'E2E_Enable', true), + ]); + await expect(poAccountSecurity.securityE2EEncryptionSection).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-key-reset.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-key-reset.spec.ts index 12bb19f1f0d9f..400ef896a30cc 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-key-reset.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-key-reset.spec.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { createAuxContext } from '../fixtures/createAuxContext'; import injectInitialData from '../fixtures/inject-initial-data'; import { Users } from '../fixtures/userStates'; -import { AccountProfile } from '../page-objects'; +import { AccountSecurity } from '../page-objects'; import { preserveSettings } from '../utils/preserveSettings'; import { test, expect } from '../utils/test'; @@ -40,12 +40,12 @@ test.describe('E2EE Key Reset', () => { }); test('expect force logout on e2e keys reset', async ({ page }) => { - const poAccountProfile = new AccountProfile(page); + const poAccountSecurity = new AccountSecurity(page); await page.goto('/account/security'); - await poAccountProfile.securityE2EEncryptionSection.click(); - await poAccountProfile.securityE2EEncryptionResetKeyButton.click(); + await poAccountSecurity.securityE2EEncryptionSection.click(); + await poAccountSecurity.securityE2EEncryptionResetKeyButton.click(); await expect(page.locator('role=button[name="Login"]')).toBeVisible(); await expect(anotherClientPage.locator('role=button[name="Login"]')).toBeVisible(); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts index ae2d2870086e7..c31419e860366 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts @@ -2,9 +2,8 @@ import { faker } from '@faker-js/faker'; import injectInitialData from '../fixtures/inject-initial-data'; import { Users, storeState, restoreState } from '../fixtures/userStates'; -import { AccountProfile, HomeChannel } from '../page-objects'; +import { AccountSecurity, HomeChannel } from '../page-objects'; import { setupE2EEPassword } from './setupE2EEPassword'; -import { AccountSecurityPage } from '../page-objects/account-security'; import { HomeSidenav } from '../page-objects/fragments'; import { E2EEKeyDecodeFailureBanner, @@ -79,7 +78,7 @@ test.describe('E2EE Passphrase Management - Initial Setup', () => { }); test('expect to manually reset the password', async ({ page }) => { - const accountSecurityPage = new AccountSecurityPage(page); + const accountSecurityPage = new AccountSecurity(page); const loginPage = new LoginPage(page); // Reset the E2EE key to start the flow from the beginning @@ -114,7 +113,7 @@ test.describe('E2EE Passphrase Management - Initial Setup', () => { }); test('expect to manually set a new password', async ({ page }) => { - const accountSecurityPage = new AccountSecurityPage(page); + const accountSecurityPage = new AccountSecurity(page); const loginPage = new LoginPage(page); const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page); const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page); @@ -183,14 +182,14 @@ test.use({ storageState: Users.admin.state }); const roomSetupSettingsList = ['E2E_Enable', 'E2E_Allow_Unencrypted_Messages']; test.describe.serial('E2EE Passphrase Management - Room Setup States', () => { - let poAccountProfile: AccountProfile; + let poAccountSecurity: AccountSecurity; let poHomeChannel: HomeChannel; let e2eePassword: string; preserveSettings(roomSetupSettingsList); test.beforeEach(async ({ page }) => { - poAccountProfile = new AccountProfile(page); + poAccountSecurity = new AccountSecurity(page); poHomeChannel = new HomeChannel(page); }); @@ -205,8 +204,8 @@ test.describe.serial('E2EE Passphrase Management - Room Setup States', () => { test('expect save password state on encrypted room', async ({ page }) => { await page.goto('/account/security'); - await poAccountProfile.securityE2EEncryptionSection.click(); - await poAccountProfile.securityE2EEncryptionResetKeyButton.click(); + await poAccountSecurity.securityE2EEncryptionSection.click(); + await poAccountSecurity.securityE2EEncryptionResetKeyButton.click(); await page.locator('role=button[name="Login"]').waitFor(); @@ -320,8 +319,8 @@ test.describe.serial('E2EE Passphrase Management - Room Setup States', () => { await page.locator('role=navigation >> a:has-text("Security")').click(); - await poAccountProfile.securityE2EEncryptionSection.click(); - await poAccountProfile.securityE2EEncryptionResetKeyButton.click(); + await poAccountSecurity.securityE2EEncryptionSection.click(); + await poAccountSecurity.securityE2EEncryptionResetKeyButton.click(); await page.locator('role=button[name="Login"]').waitFor(); diff --git a/apps/meteor/tests/e2e/enforce-2FA.spec.ts b/apps/meteor/tests/e2e/enforce-2FA.spec.ts index 41a4f77eac4f9..fae6ae3a3a9c1 100644 --- a/apps/meteor/tests/e2e/enforce-2FA.spec.ts +++ b/apps/meteor/tests/e2e/enforce-2FA.spec.ts @@ -1,6 +1,6 @@ import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; -import { HomeChannel, AccountProfile } from './page-objects'; +import { HomeChannel, AccountSecurity } from './page-objects'; import { createCustomRole, deleteCustomRole } from './utils/custom-role'; import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; @@ -11,11 +11,11 @@ test.describe('enforce two factor authentication', () => { test.skip(!IS_EE, 'Enterprise Only'); let poHomeChannel: HomeChannel; - let poAccountProfile: AccountProfile; + let poAccountSecurity: AccountSecurity; let customRoleId = ''; test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); - poAccountProfile = new AccountProfile(page); + poAccountSecurity = new AccountSecurity(page); }); test.beforeAll(async ({ api }) => { @@ -50,18 +50,18 @@ test.describe('enforce two factor authentication', () => { test('should redirect to 2FA setup page and setup email 2FA', async ({ page }) => { await page.goto('/home'); - await poAccountProfile.required2faModalSetUpButton.click(); + await poAccountSecurity.required2faModalSetUpButton.click(); await expect(poHomeChannel.sidenav.sidebarHomeAction).not.toBeVisible(); - await expect(poAccountProfile.securityHeader).toBeVisible(); + await expect(poAccountSecurity.securityHeader).toBeVisible(); - await expect(poAccountProfile.security2FASection).toHaveAttribute('aria-expanded', 'true'); - await expect(poAccountProfile.email2FASwitch).toBeVisible(); - await poAccountProfile.email2FASwitch.click(); + await expect(poAccountSecurity.security2FASection).toHaveAttribute('aria-expanded', 'true'); + await expect(poAccountSecurity.email2FASwitch).toBeVisible(); + await poAccountSecurity.email2FASwitch.click(); await poHomeChannel.toastMessage.waitForDisplay(); await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); - await expect(poAccountProfile.securityHeader).not.toBeVisible(); + await expect(poAccountSecurity.securityHeader).not.toBeVisible(); }); test.describe('should still redirect to 2FA setup page when email 2FA is disabled', () => { @@ -75,14 +75,14 @@ test.describe('enforce two factor authentication', () => { test('should redirect to 2FA setup page and show totp 2FA setup', async ({ page }) => { await page.goto('/home'); - await poAccountProfile.required2faModalSetUpButton.click(); + await poAccountSecurity.required2faModalSetUpButton.click(); await expect(poHomeChannel.sidenav.sidebarHomeAction).not.toBeVisible(); - await expect(poAccountProfile.securityHeader).toBeVisible(); + await expect(poAccountSecurity.securityHeader).toBeVisible(); - await expect(poAccountProfile.security2FASection).toHaveAttribute('aria-expanded', 'true'); - await expect(poAccountProfile.totp2FASwitch).toBeVisible(); - await expect(poAccountProfile.email2FASwitch).not.toBeVisible(); + await expect(poAccountSecurity.security2FASection).toHaveAttribute('aria-expanded', 'true'); + await expect(poAccountSecurity.totp2FASwitch).toBeVisible(); + await expect(poAccountSecurity.email2FASwitch).not.toBeVisible(); }); }); @@ -100,7 +100,7 @@ test.describe('enforce two factor authentication', () => { test('should not redirect to 2FA setup page', async ({ page }) => { await page.goto('/home'); await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); - await expect(poAccountProfile.securityHeader).not.toBeVisible(); + await expect(poAccountSecurity.securityHeader).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index e9e63fd0af2f2..0b6a47fe1abe6 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -1,14 +1,10 @@ import type { Locator, Page } from '@playwright/test'; import { Account } from './account'; -import { AccountSidenav } from './fragments/account-sidenav'; export class AccountProfile extends Account { - readonly sidenav: AccountSidenav; - constructor(page: Page) { super(page); - this.sidenav = new AccountSidenav(page); } get inputName(): Locator { @@ -100,30 +96,6 @@ export class AccountProfile extends Account { return this.page.locator('input[type=file]'); } - get securityHeader(): Locator { - return this.page.locator('h1[data-qa-type="PageHeader-title"]:has-text("Security")'); - } - - get securityPasswordSection(): Locator { - return this.page.locator('[role="button"]:has-text("Password")'); - } - - get security2FASection(): Locator { - return this.page.locator('[role="button"]:has-text("Two Factor Authentication")'); - } - - get securityE2EEncryptionSection(): Locator { - return this.page.locator('[role="button"]:has-text("End-to-end encryption")'); - } - - get securityE2EEncryptionResetKeyButton(): Locator { - return this.page.locator("role=button[name='Reset E2EE password']"); - } - - get securityE2EEncryptionSavePasswordButton(): Locator { - return this.page.locator("role=button[name='Save changes']"); - } - getAccordionItemByName(name: string): Locator { return this.page.getByRole('button', { name, exact: true }); } @@ -136,18 +108,6 @@ export class AccountProfile extends Account { return this.page.getByRole('button', { name: 'Save changes', exact: true }); } - get email2FASwitch(): Locator { - return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via email' }) }); - } - - get totp2FASwitch(): Locator { - return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via TOTP' }) }); - } - - get required2faModalSetUpButton(): Locator { - return this.page.locator('dialog >> button'); - } - get btnDeleteMyAccount(): Locator { return this.page.getByRole('button', { name: 'Delete my account' }); } diff --git a/apps/meteor/tests/e2e/page-objects/account-security.ts b/apps/meteor/tests/e2e/page-objects/account-security.ts index ef22bf31c9de8..dba166964e60e 100644 --- a/apps/meteor/tests/e2e/page-objects/account-security.ts +++ b/apps/meteor/tests/e2e/page-objects/account-security.ts @@ -1,59 +1,103 @@ -import type { Page } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; -import { ToastMessages } from './fragments/toast-messages'; +import { Account } from './account'; +import { EnterPasswordModal } from './fragments/enter-password-modal'; -export class AccountSecurityPage { - private readonly toastMessages: ToastMessages; +export class AccountSecurity extends Account { + private readonly enterPasswordModal: EnterPasswordModal; - constructor(protected readonly page: Page) { - this.toastMessages = new ToastMessages(page); + constructor(page: Page) { + super(page); + this.enterPasswordModal = new EnterPasswordModal(page); } goto() { return this.page.goto('/account/security'); } - private get expandE2EESectionButton() { + get inputNewPassword() { + return this.page.getByRole('textbox', { name: 'New password' }); + } + + private get inputConfirmPassword() { + return this.page.getByRole('textbox', { name: 'Confirm password' }); + } + + private get btnExpandE2EESection() { return this.page.getByRole('button', { name: 'End-to-end encryption' }); } - private get resetE2EEPasswordButton() { + private get btnResetE2EEPassword() { return this.page.getByRole('button', { name: 'Reset E2EE password' }); } - private get newE2EEPasswordInput() { + private get inputNewE2EEPassword() { return this.page.getByRole('textbox', { name: 'New E2EE password' }); } - private get confirmNewE2EEPasswordInput() { + private get inputConfirmNewE2EEPassword() { return this.page.getByRole('textbox', { name: 'Confirm new E2EE password' }); } - private get saveChangesButton() { - return this.page.getByRole('button', { name: 'Save changes' }); + get securityHeader(): Locator { + return this.page.locator('h1[data-qa-type="PageHeader-title"]:has-text("Security")'); + } + + get securityPasswordSection(): Locator { + return this.page.locator('[role="button"]:has-text("Password")'); + } + + get security2FASection(): Locator { + return this.page.locator('[role="button"]:has-text("Two Factor Authentication")'); + } + + get securityE2EEncryptionSection(): Locator { + return this.page.locator('[role="button"]:has-text("End-to-end encryption")'); + } + + get securityE2EEncryptionResetKeyButton(): Locator { + return this.page.locator("role=button[name='Reset E2EE password']"); + } + + get securityE2EEncryptionSavePasswordButton(): Locator { + return this.page.locator("role=button[name='Save changes']"); } - private get closeButton() { - return this.page.getByRole('navigation').getByRole('button', { name: 'Close' }); + get email2FASwitch(): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via email' }) }); + } + + get totp2FASwitch(): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via TOTP' }) }); + } + + get required2faModalSetUpButton(): Locator { + return this.page.locator('dialog >> button'); + } + + async changePassword(newPassword: string, confirmPassword: string, currentPassword: string) { + await this.inputNewPassword.fill(newPassword); + await this.inputConfirmPassword.fill(confirmPassword); + await this.saveChangesButton.click(); + await this.enterPasswordModal.enterPassword(currentPassword); } async resetE2EEPassword() { - await this.expandE2EESectionButton.click(); - await this.resetE2EEPasswordButton.click(); - await this.toastMessages.dismissToast('success'); + await this.btnExpandE2EESection.click(); + await this.btnResetE2EEPassword.click(); + await this.toastMessage.dismissToast('success'); // Logged out } async setE2EEPassword(newPassword: string) { - await this.expandE2EESectionButton.click(); - - await this.newE2EEPasswordInput.fill(newPassword); - await this.confirmNewE2EEPasswordInput.fill(newPassword); + await this.btnExpandE2EESection.click(); + await this.inputNewE2EEPassword.fill(newPassword); + await this.inputConfirmNewE2EEPassword.fill(newPassword); await this.saveChangesButton.click(); - await this.toastMessages.dismissToast('success'); + await this.toastMessage.dismissToast('success'); } async close() { - await this.closeButton.click(); + await this.sidebar.close(); } } diff --git a/apps/meteor/tests/e2e/page-objects/account.ts b/apps/meteor/tests/e2e/page-objects/account.ts index 2b562538a1399..e4726e253e7a0 100644 --- a/apps/meteor/tests/e2e/page-objects/account.ts +++ b/apps/meteor/tests/e2e/page-objects/account.ts @@ -1,11 +1,18 @@ import type { Page } from '@playwright/test'; -import { ToastMessages } from './fragments'; +import { AccountSidebar, ToastMessages } from './fragments'; export abstract class Account { readonly toastMessage: ToastMessages; + readonly sidebar: AccountSidebar; + constructor(protected page: Page) { this.toastMessage = new ToastMessages(page); + this.sidebar = new AccountSidebar(page); + } + + protected get saveChangesButton() { + return this.page.getByRole('button', { name: 'Save changes' }); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts deleted file mode 100644 index 751ce74f72803..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class AccountSidenav { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get linkSecurity(): Locator { - return this.page.locator('.flex-nav [href="/account/security"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/enter-password-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/enter-password-modal.ts new file mode 100644 index 0000000000000..95c022c88de18 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/enter-password-modal.ts @@ -0,0 +1,27 @@ +import type { Page } from 'playwright-core'; + +import { Modal } from './modal'; +import { ToastMessages } from './toast-messages'; + +export class EnterPasswordModal extends Modal { + readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Please enter your password' })); + this.toastMessages = new ToastMessages(page); + } + + private get inputPassword() { + return this.root.getByRole('textbox', { name: 'For your security, you must enter your current password to continue' }); + } + + private get btnVerify() { + return this.root.getByRole('button', { name: 'Verify' }); + } + + async enterPassword(password: string): Promise { + await this.inputPassword.fill(password); + await this.btnVerify.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 53215925e83d8..648a75c31fa3a 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -108,3 +108,18 @@ export class AdminSidebar extends Sidebar { await this.waitForDismissal(); } } + +export class AccountSidebar extends Sidebar { + constructor(page: Page) { + super(page.getByRole('navigation', { name: 'Account' })); + } + + get linkSecurity(): Locator { + return this.root.getByRole('link', { name: 'Security' }); + } + + async close(): Promise { + await this.btnClose.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index 273c37130b2d8..340b4f7fff56d 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -1,4 +1,5 @@ export * from './account-profile'; +export * from './account-security'; export * from './admin-email-inboxes'; export * from './admin-rooms'; export * from './admin-users'; diff --git a/apps/meteor/tests/e2e/utils/index.ts b/apps/meteor/tests/e2e/utils/index.ts index 7548171e8758d..9dbb7d5192b6c 100644 --- a/apps/meteor/tests/e2e/utils/index.ts +++ b/apps/meteor/tests/e2e/utils/index.ts @@ -2,3 +2,4 @@ export * from './create-target-channel'; export * from './setSettingValueById'; export * from './getSettingValueById'; export * from './setUserPreferences'; +export * from './updateOwnUserInfo'; diff --git a/apps/meteor/tests/e2e/utils/updateOwnUserInfo.ts b/apps/meteor/tests/e2e/utils/updateOwnUserInfo.ts new file mode 100644 index 0000000000000..5344ee52d0ef4 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/updateOwnUserInfo.ts @@ -0,0 +1,19 @@ +import crypto from 'crypto'; + +import type { APIResponse } from '@playwright/test'; +import type { UsersUpdateOwnBasicInfoParamsPOST } from '@rocket.chat/rest-typings'; + +import type { BaseTest } from './test'; + +export const updateOwnUserPassword = async ( + api: BaseTest['api'], + { newPassword, currentPassword }: { newPassword: string; currentPassword: string }, +): Promise => { + /** + * SHA-256 is used here for client-side password hashing before transmission (2FA verification), not for password storage. The server expects this specific format for password verification. This is a requirement of the Rocket.Chat API for the 2FA flow. */ + const encryptedPassword = crypto.createHash('sha256').update(currentPassword, 'utf8').digest('hex'); + return updateOwnUserInfo(api, { newPassword, currentPassword: encryptedPassword }); +}; + +const updateOwnUserInfo = (api: BaseTest['api'], data: UsersUpdateOwnBasicInfoParamsPOST['data']): Promise => + api.post(`/users.updateOwnBasicInfo`, { data });