diff --git a/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationDetails.page.ts b/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationDetails.page.ts index ed6fbe64976..27d5806aebd 100644 --- a/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationDetails.page.ts +++ b/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationDetails.page.ts @@ -31,6 +31,10 @@ export class ConversationDetailsPage { readonly clearConversationContentButton: Locator; readonly selectedSearchList: Locator; readonly searchList: Locator; + readonly deleteGroupButton: Locator; + readonly notificationsButton: Locator; + readonly editConversationNameButton: Locator; + readonly textFieldForConversationName: Locator; constructor(page: Page) { this.page = page; @@ -44,6 +48,10 @@ export class ConversationDetailsPage { this.clearConversationContentButton = this.conversationDetails.getByRole('button', {name: 'Clear Content'}); this.selectedSearchList = this.page.getByTestId('selected-search-list'); this.searchList = this.page.getByTestId('search-list'); + this.deleteGroupButton = this.page.getByRole('button', {name: 'Delete group'}); + this.notificationsButton = this.page.getByRole('button', {name: 'Notifications'}); + this.editConversationNameButton = this.page.getByRole('button', {name: 'Change conversation name'}); + this.textFieldForConversationName = this.page.locator('textarea[data-uie-name="enter-name"]'); } async waitForSidebar() { @@ -181,4 +189,16 @@ export class ConversationDetailsPage { async clickClearConversationContentButton() { await this.clearConversationContentButton.click(); } + + async setNotificationsForConversation(value: 'Everything' | 'Mentions and replies' | 'Nothing') { + await this.notificationsButton.click(); + const notificationsPanel = this.page.locator('aside#right-column'); + await notificationsPanel.getByRole('radiogroup').getByText(value).click(); + } + + async changeConversationName(newConversationName: string) { + await this.editConversationNameButton.click(); + await this.textFieldForConversationName.fill(newConversationName); + await this.textFieldForConversationName.press('Enter'); + } } diff --git a/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts b/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts index 9faeb13440a..d219dc57992 100644 --- a/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts +++ b/apps/webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts @@ -150,4 +150,8 @@ export class ConversationListPage { getMoveToFolderButton(folderName: string) { return this.moveToMenu.getByRole('button', {name: folderName, exact: true}); } + + async getMutedConversationBadge(conversationName: string) { + return this.getConversationLocator(conversationName).getByTitle('Muted conversation'); + } } diff --git a/apps/webapp/test/e2e_tests/specs/CriticalFlow/backupRestoration-TC-8634.spec.ts b/apps/webapp/test/e2e_tests/specs/CriticalFlow/backupRestoration-TC-8634.spec.ts index 4a5b8f99337..8369436c006 100644 --- a/apps/webapp/test/e2e_tests/specs/CriticalFlow/backupRestoration-TC-8634.spec.ts +++ b/apps/webapp/test/e2e_tests/specs/CriticalFlow/backupRestoration-TC-8634.spec.ts @@ -18,7 +18,7 @@ */ import {removeCreatedUser} from 'test/e2e_tests/utils/tearDown.util'; -import {createGroup, loginUser, logOutUser} from 'test/e2e_tests/utils/userActions'; +import {createAndSaveBackup, createGroup, loginUser, logOutUser} from 'test/e2e_tests/utils/userActions'; import {getUser} from '../../data/user'; import {test, expect} from '../../test.fixtures'; @@ -32,27 +32,9 @@ const groupMessage = 'This is a group message!'; let backupName: string; let passwordProtectedBackupName: string; -test('Setting up new device with a backup', {tag: ['@TC-8634', '@crit-flow-web']}, async ({pageManager, api}) => { +test('Setting up new device with a backup', {tag: ['@TC-8634', '@crit-flow-web']}, async ({pageManager, api}, testInfo) => { const {pages, modals, components} = pageManager.webapp; - const createAndSaveBackup = async (password?: string, filenamePrefix?: string): Promise => { - await pages.account().clickBackUpButton(); - expect(modals.passwordAdvancedSecurity().isTitleVisible()).toBeTruthy(); - if (password) { - await modals.passwordAdvancedSecurity().enterPassword(password); - } - await modals.passwordAdvancedSecurity().clickBackUpNow(); - expect(modals.passwordAdvancedSecurity().isTitleHidden()).toBeTruthy(); - expect(pages.historyExport().isVisible()).toBeTruthy(); - const [download] = await Promise.all([ - pages.historyExport().page.waitForEvent('download'), - pages.historyExport().clickSaveFileButton(), - ]); - const backupName = `./test-results/downloads/${filenamePrefix}${download.suggestedFilename()}`; - await download.saveAs(backupName); - return backupName; - }; - // Creating preconditions for the test via API await test.step('Preconditions: Creating preconditions for the test via API', async () => { await api.createPersonalUser(userA); @@ -81,11 +63,11 @@ test('Setting up new device with a backup', {tag: ['@TC-8634', '@crit-flow-web'] await test.step('User creates and saves a backup', async () => { await components.conversationSidebar().clickPreferencesButton(); - backupName = await createAndSaveBackup(); + backupName = await createAndSaveBackup(testInfo, pageManager); }); await test.step('User creates and saves a password backup', async () => { - passwordProtectedBackupName = await createAndSaveBackup(userA.password, 'password-'); + passwordProtectedBackupName = await createAndSaveBackup(testInfo, pageManager, userA.password, 'password-'); }); await test.step('User logs out and clears all data', async () => { diff --git a/apps/webapp/test/e2e_tests/specs/HistoryBackup/historyBackup.spec.ts b/apps/webapp/test/e2e_tests/specs/HistoryBackup/historyBackup.spec.ts new file mode 100644 index 00000000000..3242ab34811 --- /dev/null +++ b/apps/webapp/test/e2e_tests/specs/HistoryBackup/historyBackup.spec.ts @@ -0,0 +1,378 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {User} from 'test/e2e_tests/data/user'; +import {PageManager} from 'test/e2e_tests/pageManager'; +import {test, expect, withLogin, withConnectedUser} from 'test/e2e_tests/test.fixtures'; +import {createAndSaveBackup, createGroup, loginUser, logOutUser} from 'test/e2e_tests/utils/userActions'; +import {generateSecurePassword, generateWireEmail} from '../../utils/userDataGenerator'; +import {RequestResetPasswordPage} from '../../pageManager/webapp/pages/requestResetPassword.page'; + +test.describe('History Backup', () => { + let userA: User; + let userB: User; + + test.beforeEach(async ({createTeam}) => { + const team = await createTeam('Test Team', {withMembers: 1}); + userA = team.owner; + userB = team.members[0]; + }); + + test( + 'I want to import a backup that I exported when I was using a different email/password', + {tag: ['@TC-118', '@regression']}, + async ({createPage, api}, testInfo) => { + const [userAPageManager, userBPageManager] = await Promise.all([ + PageManager.from(createPage(withLogin(userA), withConnectedUser(userB))), + PageManager.from(createPage(withLogin(userB), withConnectedUser(userA))), + ]); + + const {pages: userAPages, modals: userAModals, components: userAComponents} = userAPageManager.webapp; + const {pages: userBPages} = userBPageManager.webapp; + + const conversationName = 'Test group'; + await createGroup(userAPages, conversationName, [userB]); + + const messageUserA = 'Message from User A'; + const messageUserB = 'Message from User B'; + + await test.step('User A and B write messages to each other', async () => { + await userAPages.conversationList().openConversation(conversationName); + await userAPages.conversation().sendMessage(messageUserA); + await userBPages.conversationList().openConversation(conversationName); + await userBPages.conversation().sendMessage(messageUserB); + }); + + // User A creates Backup + await userAComponents.conversationSidebar().clickPreferencesButton(); + const backupName = await createAndSaveBackup(testInfo, userAPageManager); + + await test.step('User A changes their Email address', async () => { + const newEmail = generateWireEmail(userA.lastName); + await userAPages.account().changeEmailAddress(newEmail); + await userAModals.acknowledge().clickAction(); // Acknowledge verify email address modal + + const activationUrl = await api.inbucket.getAccountActivationURL(newEmail); + await userAPageManager.openNewTab(activationUrl); + await userAPages.account().isDisplayedEmailEquals(newEmail); + userA.email = newEmail; + }); + + await test.step('User A changes their Password', async () => { + const [newPage] = await Promise.all([ + userAPageManager.getContext().waitForEvent('page'), + userAPages.account().clickResetPasswordButton(), + ]); + + const resetPasswordPage = new RequestResetPasswordPage(newPage); + await resetPasswordPage.requestPasswordResetForEmail(userA.email); + const resetPasswordUrl = await api.inbucket.getResetPasswordURL(userA.email); + await newPage.close(); + + const newPassword = generateSecurePassword(); + userA.password = newPassword; + + await userAPageManager.openUrl(resetPasswordUrl); + await userAPages.resetPassword().setNewPassword(newPassword); + await expect(userAPages.resetPassword().passwordChangeMessage).toBeVisible(); + + await userAPageManager.page.context().close(); + }); + + const newUserAPageManager = PageManager.from(await createPage(withLogin(userA, {confirmNewHistory: true}))); + + const {pages: userAPages2, components: userAComponents2} = newUserAPageManager.webapp; + + await userAComponents2.conversationSidebar().clickPreferencesButton(); + await userAPages2.account().backupFileInput.setInputFiles(backupName); + + await test.step('Validate conversation is still visible with all messages after restoring backup', async () => { + await userAComponents2.conversationSidebar().allConverationsButton.click(); + await userAPages2.conversationList().openConversation(conversationName); + await expect(userAPages2.conversation().getMessage({sender: userB})).toContainText(messageUserB); + await expect(userAPages2.conversation().getMessage({sender: userA})).toContainText(messageUserA); + }); + }, + ); + + test( + "I should not be able to restore from the history of another person's account", + {tag: ['@TC-125', '@regression']}, + async ({createPage}, testInfo) => { + const [userAPageManager, userBPageManager] = await Promise.all([ + PageManager.from(createPage(withLogin(userA), withConnectedUser(userB))), + PageManager.from(createPage(withLogin(userB), withConnectedUser(userA))), + ]); + + const {pages: userAPages, components: userAComponents} = userAPageManager.webapp; + const {pages: userBPages, components: userBComponents} = userBPageManager.webapp; + + const messageUserA = 'Message from User A'; + const messageUserB = 'Message from User B'; + + await test.step('User A and B write messages to each other', async () => { + await userAPages.conversationList().openConversation(userB.fullName, {protocol: 'mls'}); + await userAPages.conversation().sendMessage(messageUserA); + await userBPages.conversationList().openConversation(userA.fullName, {protocol: 'mls'}); + await userBPages.conversation().sendMessage(messageUserB); + }); + + let backupName: string; + + await test.step('User A creates History Backup', async () => { + await userAComponents.conversationSidebar().clickPreferencesButton(); + backupName = await createAndSaveBackup(testInfo, userAPageManager); + }); + + await test.step('User B tries to restore User A\'s backup', async () => { + await logOutUser(userBPageManager, true); + await loginUser(userB, userBPageManager); + await userBPages.historyInfo().clickConfirmButton(); + await userBComponents.conversationSidebar().clickPreferencesButton(); + await userBPages.account().backupFileInput.setInputFiles(backupName); + }); + + await test.step('Validate User B cannot import History Backup from User A', async () => { + const errorHeadline = userBPages.historyImport().title; + const errorInfo = userBPages.historyImport().description; + await expect(errorHeadline).toBeVisible(); + await expect(errorHeadline).toHaveText('Wrong backup'); + await expect(errorInfo).toHaveText('You cannot restore history from a different account.'); + }); + }, + ); + + test( + 'I want to see new name and system message of the renamed conversation when it was renamed after export', + {tag: ['@TC-131', '@regression']}, + async ({createPage}, testInfo) => { + const [userAPageManager, userBPageManager] = await Promise.all([ + PageManager.from(createPage(withLogin(userA), withConnectedUser(userB))), + PageManager.from(createPage(withLogin(userB), withConnectedUser(userA))), + ]); + const {pages: userAPages, components: userAComponents} = userAPageManager.webapp; + const {pages: userBPages} = userBPageManager.webapp; + + const conversationName = 'Test group'; + await createGroup(userBPages, conversationName, [userA]); + + const messageUserA = 'Message from User A'; + const messageUserB = 'Message from User B'; + + const renamedConversationName = 'renamedConversationName'; + + await test.step('User A and B write in their group conversation', async () => { + await userAPages.conversationList().openConversation(conversationName); + await userAPages.conversation().sendMessage(messageUserA); + await userBPages.conversationList().openConversation(conversationName); + await userBPages.conversation().sendMessage(messageUserB); + }); + + let backupName: string; + + await test.step('User A creates History Backup', async () => { + await userAComponents.conversationSidebar().clickPreferencesButton(); + backupName = await createAndSaveBackup(testInfo, userAPageManager); + await userAComponents.conversationSidebar().allConverationsButton.click(); + await userAPages.conversationList().openConversation(userB.fullName, { protocol: 'mls' }); + }); + + await test.step('User B renames group conversation', async () => { + await userBPages.conversation().conversationInfoButton.click(); + await userBPages.conversationDetails().changeConversationName(renamedConversationName); + }); + + await test.step('User A restores the Backup', async () => { + await userAComponents.conversationSidebar().clickPreferencesButton(); + await userAPages.account().backupFileInput.setInputFiles(backupName); + }); + + await test.step('Validate User A sees renamed conversation and system message', async () => { + // User A sees renamed conversation + await userAComponents.conversationSidebar().allConverationsButton.click(); + await expect(userAPages.conversationList().getConversationLocator(renamedConversationName)).toBeVisible(); + + // User A sees system message that User B had renamed the conversation + await userAPages.conversationList().openConversation(renamedConversationName); + const renamedSystemMessage = userAPages.conversation().systemMessages.filter({hasText: `${userB.fullName} renamed the conversation`}); + await expect(renamedSystemMessage).toContainText(`${userB.fullName} renamed the conversation`); + }); + }, + ); + + test( + 'I want to have the same mute or archive state of a conversation after import', + {tag: ['@TC-133', '@regression']}, + async ({createPage}, testInfo) => { + const [userAPageManager, userBPageManager] = await Promise.all([ + PageManager.from(createPage(withLogin(userA), withConnectedUser(userB))), + PageManager.from(createPage(withLogin(userB), withConnectedUser(userA))), + ]); + const {pages: userAPages, components: userAComponents} = userAPageManager.webapp; + const {pages: userBPages} = userBPageManager.webapp; + + const conversationName = 'Test group'; + await createGroup(userAPages, conversationName, [userB]); + + const messageUserA = 'Message from User A'; + const messageUserB = 'Message from User B'; + + await test.step('User A and B write messages to each other', async () => { + await userAPages.conversationList().openConversation(conversationName); + await userAPages.conversation().sendMessage(messageUserA); + await userBPages.conversationList().openConversation(conversationName); + await userBPages.conversation().sendMessage(messageUserB); + }); + + await test.step('User A mutes group conversation with User B', async () => { + await userAPages.conversation().conversationInfoButton.click(); + await userAPages.conversationDetails().setNotificationsForConversation('Nothing'); + }); + + await test.step('User A archives 1:1 conversation with User B', async () => { + await userAPages.conversationList().openConversation(userB.fullName); + await userAPages.conversation().conversationInfoButton.click(); + await userAPages.conversationDetails().archiveButton.click(); + }); + + let backupName: string; + + await test.step('User A creates History Backup', async () => { + await userAComponents.conversationSidebar().clickPreferencesButton(); + backupName = await createAndSaveBackup(testInfo, userAPageManager); + }); + + await test.step('User A logs out and logs back in', async () => { + await logOutUser(userAPageManager, true); + await loginUser(userA, userAPageManager); + await userAPages.historyInfo().clickConfirmButton(); + }); + + await test.step('User A restores the backup', async () => { + await userAComponents.conversationSidebar().clickPreferencesButton(); + await userAPages.account().backupFileInput.setInputFiles(backupName); + }); + + await test.step('Validate muted and archived state are the same', async () => { + await userAComponents.conversationSidebar().allConverationsButton.click(); + await userAPages.conversationList().openConversation(conversationName); + await expect(await userAPages.conversationList().getMutedConversationBadge(conversationName)).toBeVisible(); + + await userAComponents.conversationSidebar().archiveButton.click(); + const archivedConversation = userAPages.conversationList().getConversationLocator(userB.fullName); + await expect(archivedConversation).toBeVisible(); + }); + }, + ); + + test( + 'I should not be able to import a backup with wrong password', + {tag: ['@TC-135', '@regression']}, + async ({createPage}, testInfo) => { + const [userAPageManager, userBPageManager] = await Promise.all([ + PageManager.from(createPage(withLogin(userA), withConnectedUser(userB))), + PageManager.from(createPage(withLogin(userB), withConnectedUser(userA))), + ]); + + const {pages: userAPages, modals: userAModals, components: userAComponents} = userAPageManager.webapp; + const {pages: userBPages} = userBPageManager.webapp; + + const messageUserA = 'Message from User A'; + const messageUserB = 'Message from User B'; + + await test.step('User A and B write messages to each other', async () => { + await userAPages.conversationList().openConversation(userB.fullName, {protocol: 'mls'}); + await userAPages.conversation().sendMessage(messageUserA); + await userBPages.conversationList().openConversation(userA.fullName, {protocol: 'mls'}); + await userBPages.conversation().sendMessage(messageUserB); + }); + + await test.step('User A creates History Backup and tries to restore it with wrong password', async () => { + // User A creates History Backup + await userAComponents.conversationSidebar().clickPreferencesButton(); + const backupName = await createAndSaveBackup(testInfo, userAPageManager, userA.password); + + await logOutUser(userAPageManager, true); + await loginUser(userA, userAPageManager); + + // User A tries to restore backup with wrong password + await userAPages.historyInfo().clickConfirmButton(); + await userAComponents.conversationSidebar().clickPreferencesButton(); + await userAPages.account().backupFileInput.setInputFiles(backupName); + await userAModals.passwordAdvancedSecurity().enterPassword('wrongPassword1.'); + await userAModals.passwordAdvancedSecurity().clickAction(); + + const errorHeadline = userAPages.historyImport().title; + const errorInfo = userAPages.historyImport().description; + await expect(errorHeadline).toBeVisible(); + await expect(errorHeadline).toHaveText('Wrong Password'); + await expect(errorInfo).toHaveText('Please verify your input and try again'); + }); + }, + ); + + test( + 'I should not see the deleted group after restore from the backup', + {tag: ['@TC-1097', '@regression']}, + async ({createPage}, testInfo) => { + const [userAPageManager, userBPageManager] = await Promise.all([ + PageManager.from(createPage(withLogin(userA), withConnectedUser(userB))), + PageManager.from(createPage(withLogin(userB), withConnectedUser(userA))), + ]); + const {pages: userAPages, modals: userAModals, components: userAComponents} = userAPageManager.webapp; + const {pages: userBPages} = userBPageManager.webapp; + + const conversationName = 'Test group'; + await createGroup(userAPages, conversationName, [userB]); + + const messageUserA = 'Message from User A'; + const messageUserB = 'Message from User B'; + + await test.step('User A and User B write messages to each other', async () => { + await userAPages.conversationList().openConversation(conversationName); + await userAPages.conversation().sendMessage(messageUserA); + await userBPages.conversationList().openConversation(conversationName); + await userBPages.conversation().sendMessage(messageUserB); + }); + + await test.step('User A deletes group conversation with User B', async () => { + await userAPages.conversation().conversationInfoButton.click(); + await userAPages.conversationDetails().deleteGroupButton.click(); + await expect(userAModals.confirm().modalTitle).toContainText('Delete group conversation?'); + await userAModals.confirm().clickAction(); + }); + + await test.step('User A creates History Backup', async () => { + await userAComponents.conversationSidebar().clickPreferencesButton(); + const backupName = await createAndSaveBackup(testInfo, userAPageManager); + + await logOutUser(userAPageManager, true); + await loginUser(userA, userAPageManager); + await userAPages.historyInfo().clickConfirmButton(); + await userAComponents.conversationSidebar().clickPreferencesButton(); + await userAPages.account().backupFileInput.setInputFiles(backupName); + }); + + await test.step('Validate deleted group conversation is no longer visible', async () => { + await userAComponents.conversationSidebar().allConverationsButton.click(); + await expect(userAPages.conversationList().getConversationLocator(conversationName)).not.toBeVisible(); + }); + }, + ); +}); diff --git a/apps/webapp/test/e2e_tests/utils/userActions.ts b/apps/webapp/test/e2e_tests/utils/userActions.ts index 146a6c35e01..494143f32ea 100644 --- a/apps/webapp/test/e2e_tests/utils/userActions.ts +++ b/apps/webapp/test/e2e_tests/utils/userActions.ts @@ -17,7 +17,7 @@ * */ -import {expect} from 'playwright/test'; +import {expect, TestInfo} from 'playwright/test'; import {ApiManagerE2E} from '../backend/apiManager.e2e'; import {User} from '../data/user'; @@ -115,3 +115,32 @@ export async function sendConnectionRequest(senderPageManager: PageManager, rece await pages.startUI().selectUsers(receiver.username); await modals.userProfile().clickConnectButton(); } + +/** + * @param testInfo is needed to create unique backup filename + */ +export async function createAndSaveBackup( + testInfo: TestInfo, + pageManager: PageManager, + password?: string, + filenamePrefix?: string, +) { + const {pages, modals} = pageManager.webapp; + + await pages.account().clickBackUpButton(); + await expect(modals.passwordAdvancedSecurity().modal).toBeVisible(); + if (password) { + await modals.passwordAdvancedSecurity().enterPassword(password); + } + await modals.passwordAdvancedSecurity().clickBackUpNow(); + await expect(modals.passwordAdvancedSecurity().modal).toBeHidden(); + await expect(pages.historyExport().exportSuccessHeadline).toBeVisible(); + const [download] = await Promise.all([ + pages.historyExport().page.waitForEvent('download'), + pages.historyExport().clickSaveFileButton(), + ]); + const safePrefix = filenamePrefix ?? ''; + const backupName = testInfo.outputPath(`${safePrefix}${download.suggestedFilename()}`); + await download.saveAs(backupName); + return backupName; +}