Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 6e55926

Browse files
authored
Cypress test for incoming user verification (#11688)
* Cypress test for incoming user verification * Fix complaint from cypress about immediate values
1 parent 966d8bd commit 6e55926

File tree

3 files changed

+178
-37
lines changed

3 files changed

+178
-37
lines changed

cypress/e2e/crypto/crypto.spec.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { CypressBot } from "../../support/bot";
2020
import { HomeserverInstance } from "../../plugins/utils/homeserver";
2121
import { UserCredentials } from "../../support/login";
2222
import {
23+
createSharedRoomWithUser,
2324
doTwoWaySasVerification,
2425
downloadKey,
2526
enableKeyBackup,
@@ -284,16 +285,7 @@ describe("Cryptography", function () {
284285
autoJoin(this.bob);
285286

286287
// we need to have a room with the other user present, so we can open the verification panel
287-
let roomId: string;
288-
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => {
289-
roomId = _room1Id;
290-
cy.log(`Created test room ${roomId}`);
291-
cy.visit(`/#/room/${roomId}`);
292-
// wait for Bob to join the room, otherwise our attempt to open his user details may race
293-
// with his join.
294-
cy.findByText("Bob joined the room").should("exist");
295-
});
296-
288+
createSharedRoomWithUser(this.bob.getUserId());
297289
verify.call(this);
298290
});
299291

@@ -305,21 +297,15 @@ describe("Cryptography", function () {
305297
autoJoin(bob);
306298

307299
// create an encrypted room
308-
cy.createRoom({ name: "TestRoom", invite: [bob.getUserId()] })
300+
createSharedRoomWithUser(bob.getUserId())
309301
.as("testRoomId")
310302
.then((roomId) => {
311303
testRoomId = roomId;
312-
cy.log(`Created test room ${roomId}`);
313-
cy.visit(`/#/room/${roomId}`);
314304

315305
// enable encryption
316306
cy.getClient().then((cli) => {
317307
cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" });
318308
});
319-
320-
// wait for Bob to join the room, otherwise our attempt to open his user details may race
321-
// with his join.
322-
cy.findByText("Bob joined the room").should("exist");
323309
});
324310
});
325311

cypress/e2e/crypto/utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
1718
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
18-
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
1919
import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api";
2020

2121
export type EmojiMapping = [emoji: string, name: string];
@@ -200,3 +200,28 @@ export function downloadKey() {
200200
cy.findByRole("button", { name: "Download" }).click();
201201
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
202202
}
203+
204+
/**
205+
* Create a shared, unencrypted room with the given user, and wait for them to join
206+
*
207+
* @param other - UserID of the other user
208+
* @param opts - other options for the createRoom call
209+
*
210+
* @returns a cypress chainable which will yield the room ID
211+
*/
212+
export function createSharedRoomWithUser(
213+
other: string,
214+
opts: Omit<ICreateRoomOpts, "invite"> = { name: "TestRoom" },
215+
): Cypress.Chainable<string> {
216+
return cy.createRoom({ ...opts, invite: [other] }).then((roomId) => {
217+
cy.log(`Created test room ${roomId}`);
218+
cy.viewRoomById(roomId);
219+
220+
// wait for the other user to join the room, otherwise our attempt to open his user details may race
221+
// with his join.
222+
cy.findByText(" joined the room", { exact: false }).should("exist");
223+
224+
// Cypress complains if we return an immediate here rather than a promise.
225+
return Promise.resolve(roomId);
226+
});
227+
}

cypress/e2e/crypto/verification.spec.ts

Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ limitations under the License.
1616

1717
import jsQR from "jsqr";
1818

19-
import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api/verification";
19+
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
20+
import type { VerificationRequest, Verifier } from "matrix-js-sdk/src/crypto-api";
2021
import { CypressBot } from "../../support/bot";
2122
import { HomeserverInstance } from "../../plugins/utils/homeserver";
2223
import { emitPromise } from "../../support/util";
2324
import { checkDeviceIsCrossSigned, doTwoWaySasVerification, logIntoElement, waitForVerificationRequest } from "./utils";
2425
import { getToast } from "../../support/toasts";
26+
import { UserCredentials } from "../../support/login";
2527

2628
/** Render a data URL and return the rendered image data */
2729
async function renderQRCode(dataUrl: string): Promise<ImageData> {
@@ -122,15 +124,9 @@ describe("Device verification", () => {
122124
/* the bot scans the QR code */
123125
cy.get<VerificationRequest>("@verificationRequest")
124126
.then(async (request: VerificationRequest) => {
125-
// because I don't know how to scrape the imagedata from the cypress browser window,
126-
// we extract the data url and render it to a new canvas.
127-
const imageData = await renderQRCode(qrCode.attr("src"));
128-
129-
// now we can decode the QR code...
130-
const result = jsQR(imageData.data, imageData.width, imageData.height);
131-
132-
// ... and feed it into the verification request.
133-
return await request.scanQRCode(new Uint8Array(result.binaryData));
127+
// feed the QR code into the verification request.
128+
const qrData = await readQrCode(qrCode);
129+
return await request.scanQRCode(qrData);
134130
})
135131
.as("verifier");
136132
});
@@ -244,15 +240,7 @@ describe("Device verification", () => {
244240
cy.findByRole("button", { name: "Start" }).click();
245241

246242
/* on the bot side, wait for the verifier to exist ... */
247-
async function awaitVerifier() {
248-
// wait for the verifier to exist
249-
while (!botVerificationRequest.verifier) {
250-
await emitPromise(botVerificationRequest, "change");
251-
}
252-
return botVerificationRequest.verifier;
253-
}
254-
255-
cy.then(() => cy.wrap(awaitVerifier())).then((verifier: Verifier) => {
243+
cy.then(() => cy.wrap(awaitVerifier(botVerificationRequest))).then((verifier: Verifier) => {
256244
// ... confirm ...
257245
botVerificationRequest.verifier.verify();
258246

@@ -268,3 +256,145 @@ describe("Device verification", () => {
268256
});
269257
});
270258
});
259+
260+
describe("User verification", () => {
261+
// note that there are other tests that check user verification works in `crypto.spec.ts`.
262+
263+
let aliceCredentials: UserCredentials;
264+
let homeserver: HomeserverInstance;
265+
let bob: CypressBot;
266+
267+
beforeEach(() => {
268+
cy.startHomeserver("default")
269+
.as("homeserver")
270+
.then((data) => {
271+
homeserver = data;
272+
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
273+
aliceCredentials = credentials;
274+
});
275+
return cy.getBot(homeserver, {
276+
displayName: "Bob",
277+
autoAcceptInvites: true,
278+
userIdPrefix: "bob_",
279+
});
280+
})
281+
.then((data) => {
282+
bob = data;
283+
});
284+
});
285+
286+
afterEach(() => {
287+
cy.stopHomeserver(homeserver);
288+
});
289+
290+
it("can receive a verification request when there is no existing DM", () => {
291+
cy.bootstrapCrossSigning(aliceCredentials);
292+
293+
// the other user creates a DM
294+
let dmRoomId: string;
295+
let bobVerificationRequest: VerificationRequest;
296+
cy.wrap(0).then(async () => {
297+
dmRoomId = await createDMRoom(bob, aliceCredentials.userId);
298+
});
299+
300+
// accept the DM
301+
cy.viewRoomByName("Bob");
302+
cy.findByRole("button", { name: "Start chatting" }).click();
303+
304+
// once Alice has joined, Bob starts the verification
305+
cy.wrap(0).then(async () => {
306+
const room = bob.getRoom(dmRoomId)!;
307+
while (room.getMember(aliceCredentials.userId)?.membership !== "join") {
308+
await new Promise((resolve) => {
309+
// @ts-ignore can't access the enum here
310+
room.once("RoomState.members", resolve);
311+
});
312+
}
313+
bobVerificationRequest = await bob.getCrypto()!.requestVerificationDM(aliceCredentials.userId, dmRoomId);
314+
});
315+
316+
// there should also be a toast
317+
getToast("Verification requested").within(() => {
318+
// it should contain the details of the requesting user
319+
cy.contains(`Bob (${bob.credentials.userId})`);
320+
321+
// Accept
322+
cy.findByRole("button", { name: "Verify Session" }).click();
323+
});
324+
325+
// request verification by emoji
326+
cy.get("#mx_RightPanel").findByRole("button", { name: "Verify by emoji" }).click();
327+
328+
cy.wrap(0)
329+
.then(async () => {
330+
/* on the bot side, wait for the verifier to exist ... */
331+
const verifier = await awaitVerifier(bobVerificationRequest);
332+
// ... confirm ...
333+
verifier.verify();
334+
return verifier;
335+
})
336+
.then((botVerifier) => {
337+
// ... and then check the emoji match
338+
doTwoWaySasVerification(botVerifier);
339+
});
340+
341+
cy.findByRole("button", { name: "They match" }).click();
342+
cy.findByText("You've successfully verified Bob!").should("exist");
343+
cy.findByRole("button", { name: "Got it" }).click();
344+
});
345+
});
346+
347+
/** Extract the qrcode out of an on-screen html element */
348+
async function readQrCode(qrCode: JQuery<HTMLElement>) {
349+
// because I don't know how to scrape the imagedata from the cypress browser window,
350+
// we extract the data url and render it to a new canvas.
351+
const imageData = await renderQRCode(qrCode.attr("src"));
352+
353+
// now we can decode the QR code.
354+
const result = jsQR(imageData.data, imageData.width, imageData.height);
355+
return new Uint8Array(result.binaryData);
356+
}
357+
358+
async function createDMRoom(client: MatrixClient, userId: string): Promise<string> {
359+
const r = await client.createRoom({
360+
// @ts-ignore can't access the enum here
361+
preset: "trusted_private_chat",
362+
// @ts-ignore can't access the enum here
363+
visibility: "private",
364+
invite: [userId],
365+
is_direct: true,
366+
initial_state: [
367+
{
368+
type: "m.room.encryption",
369+
state_key: "",
370+
content: {
371+
algorithm: "m.megolm.v1.aes-sha2",
372+
},
373+
},
374+
],
375+
});
376+
377+
const roomId = r.room_id;
378+
379+
// wait for the room to come down /sync
380+
while (!client.getRoom(roomId)) {
381+
await new Promise((resolve) => {
382+
//@ts-ignore can't access the enum here
383+
client.once("Room", resolve);
384+
});
385+
}
386+
387+
return roomId;
388+
}
389+
390+
/**
391+
* Wait for a verifier to exist for a VerificationRequest
392+
*
393+
* @param botVerificationRequest
394+
*/
395+
async function awaitVerifier(botVerificationRequest: VerificationRequest): Promise<Verifier> {
396+
while (!botVerificationRequest.verifier) {
397+
await emitPromise(botVerificationRequest, "change");
398+
}
399+
return botVerificationRequest.verifier;
400+
}

0 commit comments

Comments
 (0)