diff --git a/cypress/integration/10-user-view/user-view.spec.ts b/cypress/integration/10-user-view/user-view.spec.ts index e98a1d47f4..30c3ff23ca 100644 --- a/cypress/integration/10-user-view/user-view.spec.ts +++ b/cypress/integration/10-user-view/user-view.spec.ts @@ -27,7 +27,7 @@ describe("UserView", () => { synapse = data; cy.initTestUser(synapse, "Violet"); - cy.getBot(synapse, "Usman").as("bot"); + cy.getBot(synapse, { displayName: "Usman" }).as("bot"); }); }); diff --git a/cypress/integration/11-room-directory/room-directory.spec.ts b/cypress/integration/11-room-directory/room-directory.spec.ts index e7e3c5c9c8..18464e2071 100644 --- a/cypress/integration/11-room-directory/room-directory.spec.ts +++ b/cypress/integration/11-room-directory/room-directory.spec.ts @@ -27,7 +27,7 @@ describe("Room Directory", () => { synapse = data; cy.initTestUser(synapse, "Ray"); - cy.getBot(synapse, "Paul").as("bot"); + cy.getBot(synapse, { displayName: "Paul" }).as("bot"); }); }); diff --git a/cypress/integration/12-spotlight/spotlight.spec.ts b/cypress/integration/12-spotlight/spotlight.spec.ts index 5c0018b499..5a76d17b4c 100644 --- a/cypress/integration/12-spotlight/spotlight.spec.ts +++ b/cypress/integration/12-spotlight/spotlight.spec.ts @@ -128,11 +128,11 @@ describe("Spotlight", () => { cy.startSynapse("default").then(data => { synapse = data; cy.initTestUser(synapse, "Jim").then(() => - cy.getBot(synapse, bot1Name).then(_bot1 => { + cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => { bot1 = _bot1; }), ).then(() => - cy.getBot(synapse, bot2Name).then(_bot2 => { + cy.getBot(synapse, { displayName: bot2Name }).then(_bot2 => { // eslint-disable-next-line @typescript-eslint/no-unused-vars bot2 = _bot2; }), diff --git a/cypress/integration/5-threads/threads.spec.ts b/cypress/integration/5-threads/threads.spec.ts index 64269b1457..3decae3876 100644 --- a/cypress/integration/5-threads/threads.spec.ts +++ b/cypress/integration/5-threads/threads.spec.ts @@ -73,7 +73,7 @@ describe("Threads", () => { it("should be usable for a conversation", () => { let bot: MatrixClient; - cy.getBot(synapse, "BotBob").then(_bot => { + cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => { bot = _bot; }); diff --git a/cypress/integration/6-spaces/spaces.spec.ts b/cypress/integration/6-spaces/spaces.spec.ts index e5c03229bf..8adfb34c51 100644 --- a/cypress/integration/6-spaces/spaces.spec.ts +++ b/cypress/integration/6-spaces/spaces.spec.ts @@ -167,7 +167,7 @@ describe("Spaces", () => { it("should allow user to invite another to a space", () => { let bot: MatrixClient; - cy.getBot(synapse, "BotBob").then(_bot => { + cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => { bot = _bot; }); @@ -202,7 +202,7 @@ describe("Spaces", () => { }); getSpacePanelButton("My Space").should("exist"); - cy.getBot(synapse, "BotBob").then({ timeout: 10000 }, async bot => { + cy.getBot(synapse, { displayName: "BotBob" }).then({ timeout: 10000 }, async bot => { const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space")); await bot.invite(roomId, user.userId); }); diff --git a/cypress/integration/7-crypto/crypto.spec.ts b/cypress/integration/7-crypto/crypto.spec.ts index 6f1f7aa6c8..020ca08bd3 100644 --- a/cypress/integration/7-crypto/crypto.spec.ts +++ b/cypress/integration/7-crypto/crypto.spec.ts @@ -14,73 +14,145 @@ See the License for the specific language governing permissions and limitations under the License. */ -/// - -import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS"; +import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { SynapseInstance } from "../../plugins/synapsedocker"; +import Chainable = Cypress.Chainable; -function waitForEncryption(cli: MatrixClient, roomId: string, win: Cypress.AUTWindow): Promise { - return new Promise(resolve => { - const onEvent = () => { - cli.crypto.cryptoStore.getEndToEndRooms(null, (result) => { - if (result[roomId]) { - cli.off(win.matrixcs.ClientEvent.Event, onEvent); - resolve(); - } - }); - }; - cli.on(win.matrixcs.ClientEvent.Event, onEvent); - }); +type EmojiMapping = [emoji: string, name: string]; +interface CryptoTestContext extends Mocha.Context { + synapse: SynapseInstance; + bob: MatrixClient; } -describe("Cryptography", () => { - beforeEach(() => { - cy.startSynapse("default").as('synapse').then( - synapse => cy.initTestUser(synapse, "Alice"), - ); +const waitForVerificationRequest = (cli: MatrixClient): Promise => { + return new Promise(resolve => { + const onVerificationRequestEvent = (request: VerificationRequest) => { + // @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here + cli.off("crypto.verification.request", onVerificationRequestEvent); + resolve(request); + }; + // @ts-ignore + cli.on("crypto.verification.request", onVerificationRequestEvent); }); +}; - afterEach(() => { - cy.get('@synapse').then(synapse => cy.stopSynapse(synapse)); +const openRoomInfo = () => { + cy.get(".mx_RightPanel_roomSummaryButton").click(); + return cy.get(".mx_RightPanel"); +}; + +const checkDMRoom = () => { + cy.contains(".mx_TextualEvent", "Alice invited Bob").should("exist"); + cy.contains(".mx_RoomView_body .mx_cryptoEvent", "Encryption enabled").should("exist"); +}; + +const startDMWithBob = function(this: CryptoTestContext) { + cy.get('.mx_RoomList [aria-label="Start chat"]').click(); + cy.get('[data-test-id="invite-dialog-input"]').type(this.bob.getUserId()); + cy.contains(".mx_InviteDialog_tile_nameStack_name", "Bob").click(); + cy.contains(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name", "Bob").should("exist"); + cy.get(".mx_InviteDialog_goButton").click(); +}; + +const testMessages = function(this: CryptoTestContext) { + cy.get(".mx_BasicMessageComposer_input").should("have.focus").type("Hey!{enter}"); + cy.contains(".mx_EventTile_body", "Hey!") + .closest(".mx_EventTile") + .should("not.have.descendants", ".mx_EventTile_e2eIcon_warning") + .should("have.descendants", ".mx_EventTile_receiptSent"); + + // Bob sends a response + cy.get("@bobsRoom").then((room) => { + this.bob.sendTextMessage(room.roomId, "Hoo!"); }); + cy.contains(".mx_EventTile_body", "Hoo!") + .closest(".mx_EventTile") + .should("not.have.descendants", ".mx_EventTile_e2eIcon_warning"); +}; - it("should receive and decrypt encrypted messages", () => { - cy.get('@synapse').then(synapse => cy.getBot(synapse, "Beatrice").as('bot')); +const bobJoin = function(this: CryptoTestContext) { + cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom"); + cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist"); +}; - cy.createRoom({ - initial_state: [ - { - type: "m.room.encryption", - state_key: '', - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - ], - }).as('roomId'); +const handleVerificationRequest = (request: VerificationRequest): Chainable => { + return cy.wrap(new Promise((resolve) => { + const onShowSas = (event: ISasEvent) => { + resolve(event.sas.emoji); + verifier.off("show_sas", onShowSas); + event.confirm(); + verifier.done(); + }; - cy.all([ - cy.get('@bot'), - cy.get('@roomId'), - cy.window(), - ]).then(([bot, roomId, win]) => { - cy.inviteUser(roomId, bot.getUserId()); - cy.wrap( - waitForEncryption( - bot, roomId, win, - ).then(() => bot.sendMessage(roomId, { - body: "Top secret message", - msgtype: "m.text", - })), - ); - cy.visit("/#/room/" + roomId); + const verifier = request.beginKeyVerification("m.sas.v1"); + verifier.on("show_sas", onShowSas); + verifier.verify(); + })); +}; + +const verify = function(this: CryptoTestContext) { + const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob); + + openRoomInfo().within(() => { + cy.get(".mx_RoomSummaryCard_icon_people").click(); + cy.contains(".mx_EntityTile_name", "Bob").click(); + cy.contains(".mx_UserInfo_verifyButton", "Verify").click(); + cy.contains(".mx_AccessibleButton", "Start Verification").click(); + cy.wrap(bobsVerificationRequestPromise).then((verificationRequest: VerificationRequest) => { + verificationRequest.accept(); + return verificationRequest; + }).as("bobsVerificationRequest"); + cy.contains(".mx_AccessibleButton", "Verify by emoji").click(); + cy.get("@bobsVerificationRequest").then((request: VerificationRequest) => { + return handleVerificationRequest(request).then((emojis: EmojiMapping[]) => { + cy.get('.mx_VerificationShowSas_emojiSas_block').then((emojiBlocks) => { + emojis.forEach((emoji: EmojiMapping, index: number) => { + expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]); + }); + }); + }); }); + cy.contains(".mx_AccessibleButton", "They match").click(); + cy.contains("You've successfully verified Bob!").should("exist"); + cy.contains(".mx_AccessibleButton", "Got it").click(); + }); +}; - cy.get(".mx_RoomView_body .mx_cryptoEvent").should("contain", "Encryption enabled"); +describe("Cryptography", function() { + beforeEach(function() { + cy.startSynapse("default").as("synapse").then((synapse: SynapseInstance) => { + cy.initTestUser(synapse, "Alice"); + cy.getBot(synapse, { displayName: "Bob", autoAcceptInvites: false }).as("bob"); + }); + }); - cy.get(".mx_EventTile_body") - .contains("Top secret message") - .closest(".mx_EventTile_line") - .should("not.have.descendants", ".mx_EventTile_e2eIcon_warning"); + afterEach(function(this: CryptoTestContext) { + cy.stopSynapse(this.synapse); + }); + + it("setting up secure key backup should work", () => { + cy.openUserSettings("Security & Privacy"); + cy.contains(".mx_AccessibleButton", "Set up Secure Backup").click(); + cy.get(".mx_Dialog").within(() => { + cy.contains(".mx_Dialog_primary", "Continue").click(); + cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey"); + // Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851 + cy.contains(".mx_AccessibleButton", "Download").click(); + cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click(); + cy.contains(".mx_Dialog_title", "Setting up keys").should("exist"); + cy.contains(".mx_Dialog_title", "Setting up keys").should("not.exist"); + }); + return; + }); + + it("creating a DM should work, being e2e-encrypted / user verification", function(this: CryptoTestContext) { + cy.bootstrapCrossSigning(); + startDMWithBob.call(this); + checkDMRoom(); + bobJoin.call(this); + testMessages.call(this); + verify.call(this); }); }); diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml index 7839c69c46..842009bcae 100644 --- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -50,3 +50,6 @@ signing_key_path: "/data/localhost.signing.key" trusted_key_servers: - server_name: "matrix.org" suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts index b75633d27d..91efca9ea0 100644 --- a/cypress/support/bot.ts +++ b/cypress/support/bot.ts @@ -18,10 +18,25 @@ limitations under the License. import request from "browser-request"; -import type { MatrixClient } from "matrix-js-sdk/src/client"; +import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import { SynapseInstance } from "../plugins/synapsedocker"; import Chainable = Cypress.Chainable; +interface CreateBotOpts { + /** + * Whether the bot should automatically accept all invites. + */ + autoAcceptInvites?: boolean; + /** + * The display name to give to that bot user + */ + displayName?: string; +} + +const defaultCreateBotOptions = { + autoAcceptInvites: true, +} as CreateBotOpts; + declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { @@ -29,17 +44,30 @@ declare global { /** * Returns a new Bot instance * @param synapse the instance on which to register the bot user - * @param displayName the display name to give to the bot user + * @param opts create bot options */ - getBot(synapse: SynapseInstance, displayName?: string): Chainable; + getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable; + /** + * Let a bot join a room + * @param cli The bot's MatrixClient + * @param roomId ID of the room to join + */ + botJoinRoom(cli: MatrixClient, roomId: string): Chainable; + /** + * Let a bot join a room by name + * @param cli The bot's MatrixClient + * @param roomName Name of the room to join + */ + botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable; } } } -Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string): Chainable => { +Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable => { + opts = Object.assign({}, defaultCreateBotOptions, opts); const username = Cypress._.uniqueId("userId_"); const password = Cypress._.uniqueId("password_"); - return cy.registerUser(synapse, username, password, displayName).then(credentials => { + return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => { return cy.window({ log: false }).then(win => { const cli = new win.matrixcs.MatrixClient({ baseUrl: synapse.baseUrl, @@ -52,18 +80,37 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string): cryptoStore: new win.matrixcs.MemoryCryptoStore(), }); - cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => { - if (member.membership === "invite" && member.userId === cli.getUserId()) { - cli.joinRoom(member.roomId); - } - }); + if (opts.autoAcceptInvites) { + cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => { + if (member.membership === "invite" && member.userId === cli.getUserId()) { + cli.joinRoom(member.roomId); + } + }); + } return cy.wrap( cli.initCrypto() .then(() => cli.setGlobalErrorOnUnknownDevices(false)) .then(() => cli.startClient()) + .then(() => cli.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async func => { await func({}); }, + })) .then(() => cli), ); }); }); }); + +Cypress.Commands.add("botJoinRoom", (cli: MatrixClient, roomId: string): Chainable => { + return cy.wrap(cli.joinRoom(roomId)); +}); + +Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string): Chainable => { + const room = cli.getRooms().find((r) => r.getDefaultRoomName(cli.getUserId()) === roomName); + + if (room) { + return cy.botJoinRoom(cli, room.roomId); + } + + return cy.wrap(Promise.reject()); +}); diff --git a/cypress/support/client.ts b/cypress/support/client.ts index db27f4d2b1..bd3f6d1514 100644 --- a/cypress/support/client.ts +++ b/cypress/support/client.ts @@ -53,6 +53,10 @@ declare global { * @param data The data to store. */ setAccountData(type: string, data: object): Chainable<{}>; + /** + * Boostraps cross-signing. + */ + bootstrapCrossSigning(): Chainable; } } } @@ -103,3 +107,11 @@ Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{ return cli.setAccountData(type, data); }); }); + +Cypress.Commands.add("bootstrapCrossSigning", () => { + cy.window({ log: false }).then(win => { + win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async func => { await func({}); }, + }); + }); +}); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 1cc34db875..478a0f8d50 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -936,6 +936,7 @@ export default class InviteDialog extends React.PureComponent 0)} autoComplete="off" placeholder={hasPlaceholder ? _t("Search") : null} + data-test-id="invite-dialog-input" /> ); return (