diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index adba3b900ac..a8fbfb2e345 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -51,7 +51,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM return ( <> - {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} {!!quotes?.length && } diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 80ab11c989e..bb422e1f20b 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -46,7 +46,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem return ( <> - {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} + {isMessageEncrypted && {t('E2E_message_encrypted_placeholder')}} {!!quotes?.length && } diff --git a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx index 1511b71686d..7c3e95c3194 100644 --- a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx @@ -2,7 +2,7 @@ import { Box, CodeSnippet } from '@rocket.chat/fuselage'; import { useClipboard } from '@rocket.chat/fuselage-hooks'; import { ExternalLink } from '@rocket.chat/ui-client'; import DOMPurify from 'dompurify'; -import type { ReactElement } from 'react'; +import { useId, type ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import GenericModal from '../../components/GenericModal'; @@ -19,6 +19,7 @@ const DOCS_URL = 'https://go.rocket.chat/i/e2ee-guide'; const SaveE2EPasswordModal = ({ randomPassword, onClose, onCancel, onConfirm }: SaveE2EPasswordModalProps): ReactElement => { const { t } = useTranslation(); const { copy, hasCopied } = useClipboard(randomPassword); + const passwordId = useId(); return ( {t('E2E_password_save_text')} -

{t('Your_E2EE_password_is')}

- copy()} mbs={8}> +

{t('Your_E2EE_password_is')}

+ copy()} + mbs={8} + > {randomPassword}
diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 0a50877d689..7d5f8384fe6 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -6,20 +6,27 @@ import { createAuxContext } from './fixtures/createAuxContext'; import injectInitialData from './fixtures/inject-initial-data'; import { Users, storeState, restoreState } from './fixtures/userStates'; import { AccountProfile, HomeChannel } from './page-objects'; +import { AccountSecurityPage } from './page-objects/account-security'; +import { EncryptedRoomPage } from './page-objects/encrypted-room'; +import { HomeSidenav } from './page-objects/fragments'; +import { + E2EEKeyDecodeFailureBanner, + EnterE2EEPasswordBanner, + EnterE2EEPasswordModal, + SaveE2EEPasswordBanner, + SaveE2EEPasswordModal, +} from './page-objects/fragments/e2ee'; +import { FileUploadModal } from './page-objects/fragments/file-upload-modal'; +import { HomeFlextabExportMessages } from './page-objects/fragments/home-flextab-exportMessages'; +import { LoginPage } from './page-objects/login'; import { test, expect } from './utils/test'; -test.use({ storageState: Users.admin.state }); - -test.describe.serial('e2e-encryption initial setup', () => { - let poAccountProfile: AccountProfile; - let poHomeChannel: HomeChannel; - let password: string; - const newPassword = 'new password'; +test.beforeAll(async () => { + await injectInitialData(); +}); - test.beforeEach(async ({ page }) => { - poAccountProfile = new AccountProfile(page); - poHomeChannel = new HomeChannel(page); - }); +test.describe('initial setup', () => { + test.use({ storageState: Users.admin.state }); test.beforeAll(async ({ api }) => { await api.post('/settings/E2E_Enable', { value: true }); @@ -31,223 +38,259 @@ test.describe.serial('e2e-encryption initial setup', () => { await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); }); - test.afterEach(async ({ api }) => { - await api.recreateContext(); - }); - - test("expect reset user's e2e encryption key", async ({ page }) => { - await page.goto('/account/security'); - - // Reset key to start the flow from the beginning - // It will execute a logout - await poAccountProfile.securityE2EEncryptionSection.click(); - await poAccountProfile.securityE2EEncryptionResetKeyButton.click(); - - await page.locator('role=button[name="Login"]').waitFor(); - - await injectInitialData(); - - // Login again, check the banner to save the generated password and test it - await restoreState(page, Users.admin); - - await poHomeChannel.bannerSaveEncryptionPassword.click(); - - password = (await page.evaluate(() => localStorage.getItem('e2e.randomPassword'))) || 'undefined'; - - await expect(poHomeChannel.dialogSaveE2EEPassword).toContainText(password); + test.beforeEach(async ({ api, page }) => { + const loginPage = new LoginPage(page); - await poHomeChannel.btnSavedMyPassword.click(); + await api.post('/method.call/e2e.resetOwnE2EKey', { + message: JSON.stringify({ msg: 'method', id: '1', method: 'e2e.resetOwnE2EKey', params: [] }), + }); - await expect(poHomeChannel.bannerSaveEncryptionPassword).not.toBeVisible(); + await page.goto('/home'); + await loginPage.waitForIt(); + await loginPage.loginByUserState(Users.admin); + }); - await poHomeChannel.sidenav.logout(); + test('expect the randomly generated password to work', async ({ page }) => { + const loginPage = new LoginPage(page); + const saveE2EEPasswordBanner = new SaveE2EEPasswordBanner(page); + const saveE2EEPasswordModal = new SaveE2EEPasswordModal(page); + const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page); + const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page); + const e2EEKeyDecodeFailureBanner = new E2EEKeyDecodeFailureBanner(page); + const sidenav = new HomeSidenav(page); - await page.locator('role=button[name="Login"]').waitFor(); + // Click the banner to open the dialog to save the generated password + await saveE2EEPasswordBanner.click(); + const password = await saveE2EEPasswordModal.getPassword(); + await saveE2EEPasswordModal.confirm(); + await saveE2EEPasswordBanner.waitForDisappearance(); - await injectInitialData(); + // Log out + await sidenav.logout(); - await restoreState(page, Users.admin); + // Login again + await loginPage.loginByUserState(Users.admin); - await poHomeChannel.bannerEnterE2EEPassword.click(); + // Enter the saved password + await enterE2EEPasswordBanner.click(); + await enterE2EEPasswordModal.enterPassword(password); - await page.locator('#modal-root input').fill(password); + // No error banner + await e2EEKeyDecodeFailureBanner.expectToNotBeVisible(); + }); - await page.locator('#modal-root .rcx-button--primary').click(); + test('expect to manually reset the password', async ({ page }) => { + const accountSecurityPage = new AccountSecurityPage(page); + const loginPage = new LoginPage(page); - await expect(poHomeChannel.bannerEnterE2EEPassword).not.toBeVisible(); + // Reset the E2EE key to start the flow from the beginning + await accountSecurityPage.goto(); + await accountSecurityPage.resetE2EEPassword(); - await storeState(page, Users.admin); + await loginPage.loginByUserState(Users.admin); }); - test('expect change the e2ee password', async ({ page }) => { - await page.goto('/account/security'); + test('expect to manually set a new password', async ({ page }) => { + const accountSecurityPage = new AccountSecurityPage(page); + const loginPage = new LoginPage(page); + const saveE2EEPasswordBanner = new SaveE2EEPasswordBanner(page); + const saveE2EEPasswordModal = new SaveE2EEPasswordModal(page); + const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page); + const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page); + const e2EEKeyDecodeFailureBanner = new E2EEKeyDecodeFailureBanner(page); + const sidenav = new HomeSidenav(page); - await restoreState(page, Users.admin); + const newPassword = faker.string.uuid(); - await poAccountProfile.securityE2EEncryptionSection.click(); - await poAccountProfile.securityE2EEncryptionPassword.click(); - await poAccountProfile.securityE2EEncryptionPassword.fill(newPassword); - await poAccountProfile.securityE2EEncryptionPasswordConfirmation.fill(newPassword); - await poAccountProfile.securityE2EEncryptionSavePasswordButton.click(); + // Click the banner to open the dialog to save the generated password + await saveE2EEPasswordBanner.click(); + await saveE2EEPasswordModal.confirm(); + await saveE2EEPasswordBanner.waitForDisappearance(); - await poAccountProfile.btnClose.click(); + // Set a new password + await accountSecurityPage.goto(); + await accountSecurityPage.setE2EEPassword(newPassword); + await accountSecurityPage.close(); - await poHomeChannel.sidenav.logout(); + // Log out + await sidenav.logout(); - await page.locator('role=button[name="Login"]').waitFor(); - - await injectInitialData(); + // Login again + await loginPage.loginByUserState(Users.admin); - await restoreState(page, Users.admin, { except: ['public_key', 'private_key'] }); + // Enter the saved password + await enterE2EEPasswordBanner.click(); + await enterE2EEPasswordModal.enterPassword(newPassword); - await poHomeChannel.bannerEnterE2EEPassword.click(); + // No error banner + await e2EEKeyDecodeFailureBanner.expectToNotBeVisible(); + }); +}); - await page.locator('#modal-root input').fill(password); +test.describe('basic features', () => { + test.use({ storageState: Users.admin.state }); - await page.locator('#modal-root .rcx-button--primary').click(); + test.beforeAll(async ({ api }) => { + await api.post('/settings/E2E_Enable', { value: true }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true }); + }); - await poHomeChannel.btnNotPossibleDecodeKey.click(); + test.afterAll(async ({ api }) => { + await api.post('/settings/E2E_Enable', { value: false }); + await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false }); + }); - await page.locator('#modal-root input').fill(newPassword); + test.beforeEach(async ({ api, page }) => { + const loginPage = new LoginPage(page); - await page.locator('#modal-root .rcx-button--primary').click(); + await api.post('/method.call/e2e.resetOwnE2EKey', { + message: JSON.stringify({ msg: 'method', id: '1', method: 'e2e.resetOwnE2EKey', params: [] }), + }); - await expect(poHomeChannel.btnNotPossibleDecodeKey).not.toBeVisible(); - await expect(poHomeChannel.bannerEnterE2EEPassword).not.toBeVisible(); + await page.goto('/home'); + await loginPage.waitForIt(); + await loginPage.loginByUserState(Users.admin); }); test('expect placeholder text in place of encrypted message', async ({ page }) => { - await page.goto('/home'); + const loginPage = new LoginPage(page); + const saveE2EEPasswordBanner = new SaveE2EEPasswordBanner(page); + const saveE2EEPasswordModal = new SaveE2EEPasswordModal(page); + const encryptedRoomPage = new EncryptedRoomPage(page); + const sidenav = new HomeSidenav(page); const channelName = faker.string.uuid(); + const messageText = 'This is an encrypted message.'; - await poHomeChannel.sidenav.createEncryptedChannel(channelName); - - await expect(page).toHaveURL(`/group/${channelName}`); + await saveE2EEPasswordBanner.click(); + await saveE2EEPasswordModal.confirm(); + await saveE2EEPasswordBanner.waitForDisappearance(); - await poHomeChannel.dismissToast(); + await sidenav.createEncryptedChannel(channelName); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await expect(page).toHaveURL(`/group/${channelName}`); + await expect(encryptedRoomPage.encryptedIcon).toBeVisible(); + await expect(encryptedRoomPage.encryptionNotReadyIndicator).not.toBeVisible(); - await poHomeChannel.content.sendMessage('This is an encrypted message.'); + await encryptedRoomPage.sendMessage(messageText); + await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible(); + await expect(encryptedRoomPage.lastMessage.body).toHaveText(messageText); - await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.'); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + await sidenav.logout(); - // Logout and login - await poHomeChannel.sidenav.logout(); - await page.locator('role=button[name="Login"]').waitFor(); - await injectInitialData(); - await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); + await loginPage.loginByUserState(Users.admin); - await poHomeChannel.sidenav.openChat(channelName); + // Navigate to the encrypted channel WITHOUT entering the password - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await sidenav.openChat(channelName); + await expect(encryptedRoomPage.encryptedIcon).toBeVisible(); + await expect(encryptedRoomPage.encryptionNotReadyIndicator).toBeVisible(); - await expect(poHomeChannel.content.lastUserMessage).toContainText( + await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible(); + await expect(encryptedRoomPage.lastMessage.body).toHaveText( 'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.', ); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - - await poHomeChannel.content.lastUserMessage.hover(); - await expect(page.locator('[role=toolbar][aria-label="Message actions"]')).not.toBeVisible(); }); - test('expect placeholder text in place of encrypted file description, when non-encrypted files upload in disabled e2ee room', async ({ - page, - }) => { - await page.goto('/home'); + test('expect placeholder text in place of encrypted file upload description', async ({ page }) => { + const encryptedRoomPage = new EncryptedRoomPage(page); + const loginPage = new LoginPage(page); + const saveE2EEPasswordBanner = new SaveE2EEPasswordBanner(page); + const saveE2EEPasswordModal = new SaveE2EEPasswordModal(page); + const fileUploadModal = new FileUploadModal(page); + const sidenav = new HomeSidenav(page); const channelName = faker.string.uuid(); + const fileName = faker.system.commonFileName('txt'); + const fileDescription = faker.lorem.sentence(); - await poHomeChannel.sidenav.createEncryptedChannel(channelName); - - await poHomeChannel.sidenav.openChat(channelName); + // Click the banner to open the dialog to save the generated password + await saveE2EEPasswordBanner.click(); + await saveE2EEPasswordModal.confirm(); + await saveE2EEPasswordBanner.waitForDisappearance(); - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + // Create an encrypted channel + await sidenav.createEncryptedChannel(channelName); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); - - await test.step('disable E2EE in the room', async () => { - await poHomeChannel.tabs.kebab.click(); - - await expect(poHomeChannel.tabs.btnDisableE2E).toBeVisible(); - await poHomeChannel.tabs.btnDisableE2E.click(); - await expect(page.getByRole('dialog', { name: 'Disable encryption' })).toBeVisible(); - await page.getByRole('button', { name: 'Disable encryption' }).click(); - await poHomeChannel.dismissToast(); - // will wait till the key icon in header goes away - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toHaveCount(0); + await expect(page).toHaveURL(`/group/${channelName}`); + await expect(encryptedRoomPage.encryptedIcon).toBeVisible(); + await expect(encryptedRoomPage.encryptionNotReadyIndicator).not.toBeVisible(); + + await test.step('upload the file with encryption', async () => { + // Upload a file + await encryptedRoomPage.dragAndDropTxtFile(); + await fileUploadModal.setName(fileName); + await fileUploadModal.setDescription(fileDescription); + await fileUploadModal.send(); + + // Check the file upload + await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible(); + await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); + await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); - await page.reload(); - - await test.step('upload the file in disabled E2EE room', async () => { - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).not.toBeVisible(); - - await poHomeChannel.content.dragAndDropTxtFile(); - await poHomeChannel.content.descriptionInput.fill('any_description'); - await poHomeChannel.content.fileNameInput.fill('any_file1.txt'); - await poHomeChannel.content.btnModalConfirm.click(); + await test.step('disable encryption in the room', async () => { + await encryptedRoomPage.disableEncryption(); + await expect(encryptedRoomPage.encryptedIcon).not.toBeVisible(); + }); - await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible(); + await test.step('upload the file without encryption', async () => { + await encryptedRoomPage.dragAndDropTxtFile(); + await fileUploadModal.setName(fileName); + await fileUploadModal.setDescription(fileDescription); + await fileUploadModal.send(); - await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); + await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); + await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); - await test.step('Enable E2EE in the room', async () => { - await poHomeChannel.tabs.kebab.click(); - - await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); - await poHomeChannel.tabs.btnEnableE2E.click(); - await expect(page.getByRole('dialog', { name: 'Enable encryption' })).toBeVisible(); - await page.getByRole('button', { name: 'Enable encryption' }).click(); - await poHomeChannel.dismissToast(); - // will wait till the key icon in header appears - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toHaveCount(1); + await test.step('enable encryption in the room', async () => { + await encryptedRoomPage.enableEncryption(); + await expect(encryptedRoomPage.encryptedIcon).toBeVisible(); }); - // Logout to remove e2ee keys - await poHomeChannel.sidenav.logout(); + // Log out + await sidenav.logout(); // Login again - await page.locator('role=button[name="Login"]').waitFor(); - await injectInitialData(); - await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); + await loginPage.loginByUserState(Users.admin); - await poHomeChannel.sidenav.openChat(channelName); - - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await sidenav.openChat(channelName); + await expect(encryptedRoomPage.encryptedIcon).toBeVisible(); - await expect(poHomeChannel.content.nthMessage(0)).toContainText( + await expect(encryptedRoomPage.lastNthMessage(1).body).toHaveText( 'This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.', ); - await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible(); + await expect(encryptedRoomPage.lastNthMessage(1).encryptedIcon).toBeVisible(); + + await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); + await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); + await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); test('should display only the download file method when exporting messages in an e2ee room', async ({ page }) => { - await page.goto('/home'); + const sidenav = new HomeSidenav(page); + const encryptedRoomPage = new EncryptedRoomPage(page); + const exportMessagesTab = new HomeFlextabExportMessages(page); + const channelName = faker.string.uuid(); - await poHomeChannel.sidenav.createEncryptedChannel(channelName); - await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await sidenav.createEncryptedChannel(channelName); + await expect(page).toHaveURL(`/group/${channelName}`); + await expect(encryptedRoomPage.encryptedRoomHeaderIcon).toBeVisible(); - await poHomeChannel.tabs.kebab.click({ force: true }); - await poHomeChannel.tabs.btnExportMessages.click(); - await expect(poHomeChannel.tabs.exportMessages.downloadFileMethod).toBeVisible(); + await encryptedRoomPage.showExportMessagesTab(); + await expect(exportMessagesTab.downloadFileMethod).toBeVisible(); + await expect(exportMessagesTab.sendEmailMethod).not.toBeVisible(); }); }); +test.use({ storageState: Users.admin.state }); + test.describe.serial('e2e-encryption', () => { + // test.skip(); + let poHomeChannel: HomeChannel; test.use({ storageState: Users.userE2EE.state }); @@ -275,8 +318,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('hello world'); @@ -319,8 +360,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('This is the thread main message.'); @@ -353,8 +392,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('This is an encrypted message.'); @@ -406,8 +443,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('hello @user1'); @@ -426,8 +461,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('Are you in the #general channel?'); @@ -450,8 +483,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('Are you in the #general channel, @user1 ?'); @@ -667,8 +698,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('This is an encrypted message.'); @@ -710,8 +739,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('This is an encrypted message.'); @@ -778,8 +805,6 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); await poHomeChannel.content.sendMessage('This message should be pinned and stared.'); @@ -849,6 +874,8 @@ test.describe.serial('e2e-encryption', () => { }); test.describe.serial('e2ee room setup', () => { + // test.skip(); + let poAccountProfile: AccountProfile; let poHomeChannel: HomeChannel; let e2eePassword: string; @@ -1040,6 +1067,8 @@ test.describe.serial('e2ee room setup', () => { }); test.describe('e2ee support legacy formats', () => { + // test.skip(); + test.use({ storageState: Users.userE2EE.state }); let poHomeChannel: HomeChannel; @@ -1068,8 +1097,6 @@ test.describe('e2ee support legacy formats', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); const rid = await page.locator('[data-qa-rc-room]').getAttribute('data-qa-rc-room'); diff --git a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts index 56a08bf120e..46b019794f6 100644 --- a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts +++ b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts @@ -1,3 +1,4 @@ +import type { ISetting, IUser } from '@rocket.chat/core-typings'; import { MongoClient } from 'mongodb'; import * as constants from '../config/constants'; @@ -23,7 +24,7 @@ export default async function injectInitialData() { await connection .db() - .collection('users') + .collection('users') .updateOne( { username: Users.admin.data.username }, { $addToSet: { 'services.resume.loginTokens': { when: Users.admin.data.loginExpire, hashedToken: Users.admin.data.hashedToken } } }, @@ -74,8 +75,8 @@ export default async function injectInitialData() { ].map((setting) => connection .db() - .collection('rocketchat_settings') - .updateOne({ _id: setting._id as any }, { $set: { value: setting.value } }), + .collection('rocketchat_settings') + .updateOne({ _id: setting._id }, { $set: { value: setting.value } }), ), ); diff --git a/apps/meteor/tests/e2e/page-objects/account-security.ts b/apps/meteor/tests/e2e/page-objects/account-security.ts new file mode 100644 index 00000000000..500f19ec27f --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/account-security.ts @@ -0,0 +1,51 @@ +import type { Page } from '@playwright/test'; + +export class AccountSecurityPage { + constructor(protected readonly page: Page) {} + + goto() { + return this.page.goto('/account/security'); + } + + private get expandE2EESectionButton() { + return this.page.getByRole('button', { name: 'End-to-end encryption' }); + } + + private get resetE2EEPasswordButton() { + return this.page.getByRole('button', { name: 'Reset E2EE key' }); + } + + private get newE2EEPasswordInput() { + return this.page.getByRole('textbox', { name: 'New encryption password' }); + } + + private get confirmNewE2EEPasswordInput() { + return this.page.getByRole('textbox', { name: 'Confirm new encryption password' }); + } + + private get saveChangesButton() { + return this.page.getByRole('button', { name: 'Save changes' }); + } + + private get closeButton() { + return this.page.getByRole('navigation').getByRole('button', { name: 'Close' }); + } + + async resetE2EEPassword() { + await this.expandE2EESectionButton.click(); + await this.resetE2EEPasswordButton.click(); + // Logged out + } + + async setE2EEPassword(newPassword: string) { + await this.expandE2EESectionButton.click(); + + await this.newE2EEPasswordInput.fill(newPassword); + await this.confirmNewE2EEPasswordInput.fill(newPassword); + await this.saveChangesButton.click(); + } + + async close() { + await this.closeButton.click(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/encrypted-room.ts b/apps/meteor/tests/e2e/page-objects/encrypted-room.ts new file mode 100644 index 00000000000..c715faad441 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/encrypted-room.ts @@ -0,0 +1,47 @@ +import { HomeContent, HomeFlextab } from './fragments'; +import { DisableRoomEncryptionModal, EnableRoomEncryptionModal } from './fragments/e2ee'; +import { Message } from './fragments/message'; + +export class EncryptedRoomPage extends HomeContent { + get encryptedIcon() { + return this.page.locator('.rcx-room-header i.rcx-icon--name-key'); + } + + get encryptionNotReadyIndicator() { + return this.page.getByText("You're sending an unencrypted message"); + } + + get lastMessage() { + return new Message(this.page.locator('[data-qa-type="message"]').last()); + } + + lastNthMessage(index: number) { + return new Message(this.page.locator(`[data-qa-type="message"]`).nth(-index - 1)); + } + + async enableEncryption() { + const tabs = new HomeFlextab(this.page); + + const enableRoomEncryptionModal = new EnableRoomEncryptionModal(this.page); + + await tabs.kebab.click(); + await tabs.btnEnableE2E.click(); + await enableRoomEncryptionModal.enable(); + } + + async disableEncryption() { + const tabs = new HomeFlextab(this.page); + const disableRoomEncryptionModal = new DisableRoomEncryptionModal(this.page); + + await tabs.kebab.click(); + await tabs.btnDisableE2E.click(); + await disableRoomEncryptionModal.disable(); + } + + async showExportMessagesTab() { + const tabs = new HomeFlextab(this.page); + + await tabs.kebab.click(); + await tabs.btnExportMessages.click(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts b/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts new file mode 100644 index 00000000000..c5ebd1ce140 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts @@ -0,0 +1,125 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Modal } from './modal'; +import { ToastMessages } from './toast-messages'; +import { expect } from '../../utils/test'; + +abstract class E2EEBanner { + constructor(protected root: Locator) {} + + click() { + return this.root.click(); + } + + async waitForDisappearance() { + await expect(this.root).not.toBeVisible(); + } +} + +export class SaveE2EEPasswordBanner extends E2EEBanner { + constructor(page: Page) { + super(page.getByRole('button', { name: 'Save your encryption password' })); + } +} + +export class EnterE2EEPasswordBanner extends E2EEBanner { + constructor(page: Page) { + // TODO: there is a typo in the default translation + super(page.getByRole('button', { name: 'Enter your E2E password' })); + } +} + +export class E2EEKeyDecodeFailureBanner extends E2EEBanner { + constructor(page: Page) { + super(page.getByRole('button', { name: "Wasn't possible to decode your encryption key to be imported." })); + } + + async expectToNotBeVisible() { + await expect(this.root).not.toBeVisible(); + } +} + +export class SaveE2EEPasswordModal extends Modal { + private readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Save your encryption password' })); + this.toastMessages = new ToastMessages(page); + } + + private get password() { + return this.root.getByLabel('Your E2EE password is:').getByRole('code'); + } + + private get savedPasswordButton() { + return this.root.getByRole('button', { name: 'I saved my password' }); + } + + async getPassword() { + return (await this.password.textContent()) ?? ''; + } + + async confirm() { + await this.savedPasswordButton.click(); + await this.waitForDismissal(); + await this.toastMessages.dismissToast('success'); + } +} + +export class EnterE2EEPasswordModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Enter E2EE password' })); + } + + private get passwordInput() { + return this.root.getByPlaceholder('Please enter your E2EE password'); + } + + private get enterE2EEPasswordButton() { + return this.root.getByRole('button', { name: 'Enable encryption' }); + } + + async enterPassword(password: string) { + await this.passwordInput.fill(password); + await this.enterE2EEPasswordButton.click(); + await this.waitForDismissal(); + } +} + +export class EnableRoomEncryptionModal extends Modal { + private readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Enable encryption' })); + this.toastMessages = new ToastMessages(page); + } + + private get enableButton() { + return this.root.getByRole('button', { name: 'Enable encryption' }); + } + + async enable() { + await this.enableButton.click(); + await this.waitForDismissal(); + await this.toastMessages.dismissToast('success'); + } +} + +export class DisableRoomEncryptionModal extends Modal { + private readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Disable encryption' })); + this.toastMessages = new ToastMessages(page); + } + + private get disableButton() { + return this.root.getByRole('button', { name: 'Disable encryption' }); + } + + async disable() { + await this.disableButton.click(); + await this.waitForDismissal(); + await this.toastMessages.dismissToast('success'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts new file mode 100644 index 00000000000..92f2915827e --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts @@ -0,0 +1,34 @@ +import type { Page } from '@playwright/test'; + +import { Modal } from './modal'; + +export class FileUploadModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'File Upload' })); + } + + private get fileNameInput() { + return this.root.getByRole('textbox', { name: 'File name' }); + } + + private get fileDescriptionInput() { + return this.root.getByRole('textbox', { name: 'File description' }); + } + + private get sendButton() { + return this.root.getByRole('button', { name: 'Send' }); + } + + setName(fileName: string) { + return this.fileNameInput.fill(fileName); + } + + setDescription(description: string) { + return this.fileDescriptionInput.fill(description); + } + + async send() { + await this.sendButton.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 5fcb7a044fe..f60b8fbcc34 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -489,4 +489,8 @@ export class HomeContent { get btnDismissContactUnknownCallout() { return this.contactUnknownCallout.getByRole('button', { name: 'Dismiss' }); } + + async expectLastMessageToHaveText(text: string): Promise { + await expect(this.lastUserMessageBody).toHaveText(text); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 872c6b46b7d..53fe5033ab2 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -1,5 +1,6 @@ import type { Locator, Page } from '@playwright/test'; +import { ToastMessages } from './toast-messages'; import { expect } from '../../utils/test'; export class HomeSidenav { @@ -211,10 +212,14 @@ export class HomeSidenav { } async createEncryptedChannel(name: string) { + const toastMessages = new ToastMessages(this.page); + await this.openNewByLabel('Channel'); await this.inputChannelName.fill(name); await this.advancedSettingsAccordion.click(); await this.checkboxEncryption.click(); await this.btnCreate.click(); + + await toastMessages.dismissToast('success'); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/message.ts b/apps/meteor/tests/e2e/page-objects/fragments/message.ts new file mode 100644 index 00000000000..77dee24c85c --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/message.ts @@ -0,0 +1,17 @@ +import type { Locator } from '@playwright/test'; + +export class Message { + constructor(public readonly root: Locator) {} + + get body() { + return this.root.locator('[data-qa-type="message-body"]'); + } + + get fileUploadName() { + return this.root.locator('[data-qa-type="attachment-title-link"]'); + } + + get encryptedIcon() { + return this.root.locator('.rcx-icon--name-key'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modal.ts new file mode 100644 index 00000000000..007dfd5cb3c --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modal.ts @@ -0,0 +1,11 @@ +import type { Locator } from '@playwright/test'; + +import { expect } from '../../utils/test'; + +export abstract class Modal { + constructor(protected root: Locator) {} + + waitForDismissal() { + return expect(this.root).not.toBeVisible(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/toast-messages.ts b/apps/meteor/tests/e2e/page-objects/fragments/toast-messages.ts new file mode 100644 index 00000000000..00fd5616bf3 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/toast-messages.ts @@ -0,0 +1,14 @@ +import type { Page } from '@playwright/test'; + +export class ToastMessages { + constructor(private readonly page: Page) {} + + private readonly toastByType = { + success: this.page.locator('.rcx-toastbar.rcx-toastbar--success'), + }; + + async dismissToast(type: 'success') { + await this.toastByType[type].locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); + await this.page.mouse.move(0, 0); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/login.ts b/apps/meteor/tests/e2e/page-objects/login.ts new file mode 100644 index 00000000000..1810d27cdcb --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/login.ts @@ -0,0 +1,48 @@ +import type { Page } from '@playwright/test'; +import type { IUser } from '@rocket.chat/core-typings'; +import { MongoClient } from 'mongodb'; + +import * as constants from '../config/constants'; +import type { IUserState } from '../fixtures/userStates'; +import { expect } from '../utils/test'; + +export class LoginPage { + constructor(protected readonly page: Page) {} + + get loginButton() { + return this.page.getByRole('button', { name: 'Login' }); + } + + /** @deprecated ideally the previous action should ensure the user is logged out and we should just assume to be at the login page */ + async waitForIt() { + await this.loginButton.waitFor(); + } + + protected async waitForLogin() { + await expect(this.loginButton).not.toBeVisible(); + } + + async loginByUserState(userState: IUserState) { + // Creates a login token for the user + const connection = await MongoClient.connect(constants.URL_MONGODB); + + await connection + .db() + .collection('users') + .updateOne( + { username: userState.data.username }, + { $addToSet: { 'services.resume.loginTokens': { when: userState.data.loginExpire, hashedToken: userState.data.hashedToken } } }, + ); + + await connection.close(); + + // Injects the login token to the local storage + await this.page.evaluate((items) => { + items.forEach(({ name, value }) => { + window.localStorage.setItem(name, value); + }); + }, userState.state.origins[0].localStorage); + + await this.waitForLogin(); + } +}