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 (