diff --git a/playwright/e2e/crypto/history-sharing.spec.ts b/playwright/e2e/crypto/history-sharing.spec.ts index cdaa543bd97..ee275da84ac 100644 --- a/playwright/e2e/crypto/history-sharing.spec.ts +++ b/playwright/e2e/crypto/history-sharing.spec.ts @@ -56,4 +56,85 @@ test.describe("History sharing", function () { }); }, ); + + test("Messages sent when we believed the room history was unshared should not be visible", async ({ + labsFlags, + browser, + page: alicePage, + user: aliceCredentials, + app: aliceElementApp, + homeserver, + }, testInfo) => { + test.setTimeout(60000); + + // In this test: + // 1. Alice creates an encrypted room with Bob. + // 2. She sets the history visibility to "shared", but Bob doesn't receive the memo + // 3. Bob sends a message + // 4. Alice invites Charlie + // 5. Charlie can't see the message. + + await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials); + await createRoom(alicePage, "TestRoom", true); + + // Register a second user, and open it in a second instance of the app + const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "Bob"); + const bobPage = await createNewInstance(browser, bobCredentials, {}, labsFlags); + const bobElementApp = new ElementAppPage(bobPage); + await bobElementApp.client.bootstrapCrossSigning(bobCredentials); + + // ... and a third + const charlieCredentials = await homeserver.registerUser( + `user_${testInfo.testId}_charlie`, + "password", + "Charlie", + ); + const charliePage = await createNewInstance(browser, charlieCredentials, {}, labsFlags); + const charlieElementApp = new ElementAppPage(charliePage); + await charlieElementApp.client.bootstrapCrossSigning(charlieCredentials); + + // Alice invites Bob, and Bob accepts + const roomId = await aliceElementApp.getCurrentRoomIdFromUrl(); + await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId); + await bobPage.getByRole("option", { name: "TestRoom" }).click(); + await bobPage.getByRole("button", { name: "Accept" }).click(); + + // Bob sends a message with "shared" visibility + await sendMessageInCurrentRoom(bobPage, "Message1: 'shared' visibility"); + await expect(alicePage.getByText("Message1")).toBeVisible(); + + // Alice sets the history visibility to "joined" + await aliceElementApp.client.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "joined", + }); + await expect( + bobPage.getByText( + "Alice made future room history visible to all room members, from the point they joined.", + ), + ).toBeVisible(); + + // Bob stops syncing, and sends a message with "joined" visibility. + // (Stopping syncing *before* sending the message means that the active sync will be flushed by sending the + // message, so that Alice's change to the history viz below won't be seen by Bob.) + await bobPage.route(`**/sync*`, (route) => route.fulfill({})); + await sendMessageInCurrentRoom(bobPage, "Message2: 'joined' visibility"); + await expect(alicePage.getByText("Message2")).toBeVisible(); + + // Alice changes the history viz, but Bob doesn't receive the memo + await aliceElementApp.client.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "shared", + }); + await sendMessageInCurrentRoom(bobPage, "Message3: 'shared' visibility, but Bob thinks it is still 'joined'"); + + // Alice now invites Charlie + await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId); + await charliePage.getByRole("option", { name: "TestRoom" }).click(); + await charliePage.getByRole("button", { name: "Accept" }).click(); + + // Message1 should be visible + // Message2 should be invisible + // Message3 should be undecryptable + await expect(charliePage.getByText("Message1")).toBeVisible(); + await expect(charliePage.getByText("You don't have access to this message")).toBeVisible(); + }); }); diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index d7689230e1c..d53aec8081f 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -199,6 +199,21 @@ export class ElementAppPage { return this.page.locator(".mx_RightPanel"); } + /** + * Opens the room info panel if it is not already open. + * + * TODO: fix this so that it works correctly if, say, the member list was open instead of the room info panel. + * + * @returns locator to the right panel + */ + public async openRoomInfoPanel(): Promise { + const locator = this.page.getByTestId("right-panel"); + if (!(await locator.isVisible())) { + await this.page.getByRole("button", { name: "Room info" }).first().click(); + } + return locator; + } + /** * Opens/closes the memberlist panel * @returns locator to the memberlist panel @@ -217,8 +232,8 @@ export class ElementAppPage { * @param userId - The user to invite to the room. */ public async inviteUserToCurrentRoom(userId: string): Promise { - await this.toggleRoomInfoPanel(); // TODO skip this if the room info panel is already open - await this.page.getByTestId("right-panel").getByRole("menuitem", { name: "Invite" }).click(); + const rightPanel = await this.openRoomInfoPanel(); + await rightPanel.getByRole("menuitem", { name: "Invite" }).click(); const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input"); await input.fill(userId);