Cypress tests for event shields (#11525)

* Factor downloadKey out to `utils.ts`

* Add a new `describe` block for event shields

* create a beforeEach block

* Cypress tests for event shields
This commit is contained in:
Richard van der Hoff 2023-09-05 12:11:10 +01:00 committed by GitHub
parent fca9f0e91d
commit 3818c1dc70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 284 additions and 75 deletions

View file

@ -19,7 +19,14 @@ import type { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import type { CypressBot } from "../../support/bot";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login";
import { doTwoWaySasVerification, waitForVerificationRequest } from "./utils";
import {
doTwoWaySasVerification,
downloadKey,
enableKeyBackup,
logIntoElement,
logOutOfElement,
waitForVerificationRequest,
} from "./utils";
import { skipIfRustCrypto } from "../../support/util";
interface CryptoTestContext extends Mocha.Context {
@ -129,19 +136,26 @@ const verify = function (this: CryptoTestContext) {
describe("Cryptography", function () {
let aliceCredentials: UserCredentials;
let homeserver: HomeserverInstance;
let bob: CypressBot;
beforeEach(function () {
cy.startHomeserver("default")
.as("homeserver")
.then((homeserver: HomeserverInstance) => {
.then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
aliceCredentials = credentials;
});
cy.getBot(homeserver, {
return cy.getBot(homeserver, {
displayName: "Bob",
autoAcceptInvites: false,
userIdPrefix: "bob_",
}).as("bob");
});
})
.as("bob")
.then((data) => {
bob = data;
});
});
@ -169,15 +183,6 @@ describe("Cryptography", function () {
});
}
/**
* Click on download button and continue
*/
function downloadKey() {
// Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
cy.findByRole("button", { name: "Download" }).click();
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
}
it("by recovery code", () => {
skipIfRustCrypto();
@ -294,53 +299,217 @@ describe("Cryptography", function () {
verify.call(this);
});
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
skipIfRustCrypto();
cy.bootstrapCrossSigning(aliceCredentials);
describe("event shields", () => {
let testRoomId: string;
// bob has a second, not cross-signed, device
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
beforeEach(() => {
cy.bootstrapCrossSigning(aliceCredentials);
autoJoin(bob);
autoJoin(this.bob);
// create an encrypted room
cy.createRoom({ name: "TestRoom", invite: [bob.getUserId()] })
.as("testRoomId")
.then((roomId) => {
testRoomId = roomId;
cy.log(`Created test room ${roomId}`);
cy.visit(`/#/room/${roomId}`);
// first create the room, so that we can open the verification panel
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] })
.as("testRoomId")
.then((roomId) => {
cy.log(`Created test room ${roomId}`);
cy.visit(`/#/room/${roomId}`);
// enable encryption
cy.getClient().then((cli) => {
cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" });
});
// enable encryption
cy.getClient().then((cli) => {
cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" });
// wait for Bob to join the room, otherwise our attempt to open his user details may race
// with his join.
cy.findByText("Bob joined the room").should("exist");
});
});
// wait for Bob to join the room, otherwise our attempt to open his user details may race
// with his join.
cy.findByText("Bob joined the room").should("exist");
it("should show the correct shield on e2e events", function (this: CryptoTestContext) {
skipIfRustCrypto();
// Bob has a second, not cross-signed, device
let bobSecondDevice: MatrixClient;
cy.loginBot(homeserver, bob.getUserId(), bob.__cypress_password, {}).then(async (data) => {
bobSecondDevice = data;
});
verify.call(this);
/* Should show an error for a decryption failure */
cy.wrap(0).then(() =>
bob.sendEvent(testRoomId, "m.room.encrypted", {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "the bird is in the hand",
}),
);
cy.get<string>("@testRoomId").then((roomId) => {
cy.get(".mx_EventTile_last")
.should("contain", "Unable to decrypt message")
.find(".mx_EventTile_e2eIcon")
.should("have.class", "mx_EventTile_e2eIcon_decryption_failure")
.should("have.attr", "aria-label", "This message could not be decrypted");
/* Should show a red padlock for an unencrypted message in an e2e room */
cy.wrap(0)
.then(() =>
bob.http.authedRequest<ISendEventResponse>(
// @ts-ignore-next this wants a Method instance, but that is hard to get to here
"PUT",
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
undefined,
{
msgtype: "m.text",
body: "test unencrypted",
},
),
)
.then((resp) => cy.log(`Bob sent unencrypted event with event id ${resp.event_id}`));
cy.get(".mx_EventTile_last")
.should("contain", "test unencrypted")
.find(".mx_EventTile_e2eIcon")
.should("have.class", "mx_EventTile_e2eIcon_warning")
.should("have.attr", "aria-label", "Unencrypted");
/* Should show no padlock for an unverified user */
// bob sends a valid event
cy.wrap(this.bob.sendTextMessage(roomId, "Hoo!")).as("testEvent");
cy.wrap(0)
.then(() => bob.sendTextMessage(testRoomId, "test encrypted 1"))
.then((resp) => cy.log(`Bob sent message from primary device with event id ${resp.event_id}`));
// the message should appear, decrypted, with no warning
cy.get(".mx_EventTile_last .mx_EventTile_body")
.within(() => {
cy.findByText("Hoo!");
})
.closest(".mx_EventTile")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
// the message should appear, decrypted, with no warning, but also no "verified"
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted 1")
// no e2e icon
.should("not.have.descendants", ".mx_EventTile_e2eIcon");
// bob sends an edit to the first message with his unverified device
cy.get<MatrixClient>("@bobSecondDevice").then((bobSecondDevice) => {
/* Now verify Bob */
verify.call(this);
/* Existing message should be updated when user is verified. */
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted 1")
// still no e2e icon
.should("not.have.descendants", ".mx_EventTile_e2eIcon");
/* should show no padlock, and be verified, for a message from a verified device */
cy.wrap(0)
.then(() => bob.sendTextMessage(testRoomId, "test encrypted 2"))
.then((resp) => cy.log(`Bob sent second message from primary device with event id ${resp.event_id}`));
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted 2")
// no e2e icon
.should("not.have.descendants", ".mx_EventTile_e2eIcon");
/* should show red padlock for a message from an unverified device */
cy.wrap(0)
.then(() => bobSecondDevice.sendTextMessage(testRoomId, "test encrypted from unverified"))
.then((resp) => cy.log(`Bob sent message from unverified device with event id ${resp.event_id}`));
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted from unverified")
.find(".mx_EventTile_e2eIcon", { timeout: 100000 })
.should("have.class", "mx_EventTile_e2eIcon_warning")
.should("have.attr", "aria-label", "Encrypted by an unverified session");
/* Should show a grey padlock for a message from an unknown device */
// bob deletes his second device, making the encrypted event from the unverified device "unknown".
cy.wrap(0)
.then(() => bobSecondDevice.logout(true))
.then(() => cy.log(`Bob logged out second device`));
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted from unverified")
.find(".mx_EventTile_e2eIcon")
.should("have.class", "mx_EventTile_e2eIcon_normal")
.should("have.attr", "aria-label", "Encrypted by a deleted session");
});
it("Should show a grey padlock for a key restored from backup", () => {
skipIfRustCrypto();
enableKeyBackup();
// bob sends a valid event
cy.wrap(0)
.then(() => bob.sendTextMessage(testRoomId, "test encrypted 1"))
.then((resp) => cy.log(`Bob sent message from primary device with event id ${resp.event_id}`));
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted 1")
// no e2e icon
.should("not.have.descendants", ".mx_EventTile_e2eIcon");
/* log out, and back i */
logOutOfElement();
cy.get<string>("@securityKey").then((securityKey) => {
logIntoElement(homeserver.baseUrl, aliceCredentials.username, aliceCredentials.password, securityKey);
});
/* go back to the test room and find Bob's message again */
cy.viewRoomById(testRoomId);
cy.get(".mx_EventTile_last")
.should("contain", "test encrypted 1")
.find(".mx_EventTile_e2eIcon")
.should("have.class", "mx_EventTile_e2eIcon_normal")
.should(
"have.attr",
"aria-label",
"The authenticity of this encrypted message can't be guaranteed on this device.",
);
});
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
skipIfRustCrypto();
// bob has a second, not cross-signed, device
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
// verify Bob
verify.call(this);
cy.get<string>("@testRoomId").then((roomId) => {
// bob sends a valid event
cy.wrap(this.bob.sendTextMessage(roomId, "Hoo!")).as("testEvent");
// the message should appear, decrypted, with no warning
cy.get(".mx_EventTile_last .mx_EventTile_body")
.within(() => {
cy.findByText("Hoo!");
})
.closest(".mx_EventTile")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
// bob sends an edit to the first message with his unverified device
cy.get<MatrixClient>("@bobSecondDevice").then((bobSecondDevice) => {
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
bobSecondDevice.sendMessage(roomId, {
"m.new_content": {
msgtype: "m.text",
body: "Haa!",
},
"m.relates_to": {
rel_type: "m.replace",
event_id: testEvent.event_id,
},
});
});
});
// the edit should have a warning
cy.contains(".mx_EventTile_body", "Haa!")
.closest(".mx_EventTile")
.within(() => {
cy.get(".mx_EventTile_e2eIcon_warning").should("exist");
});
// a second edit from the verified device should be ok
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
bobSecondDevice.sendMessage(roomId, {
this.bob.sendMessage(roomId, {
"m.new_content": {
msgtype: "m.text",
body: "Haa!",
body: "Hee!",
},
"m.relates_to": {
rel_type: "m.replace",
@ -348,35 +517,14 @@ describe("Cryptography", function () {
},
});
});
cy.get(".mx_EventTile_last .mx_EventTile_body")
.within(() => {
cy.findByText("Hee!");
})
.closest(".mx_EventTile")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
});
// the edit should have a warning
cy.contains(".mx_EventTile_body", "Haa!")
.closest(".mx_EventTile")
.within(() => {
cy.get(".mx_EventTile_e2eIcon_warning").should("exist");
});
// a second edit from the verified device should be ok
cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
this.bob.sendMessage(roomId, {
"m.new_content": {
msgtype: "m.text",
body: "Hee!",
},
"m.relates_to": {
rel_type: "m.replace",
event_id: testEvent.event_id,
},
});
});
cy.get(".mx_EventTile_last .mx_EventTile_body")
.within(() => {
cy.findByText("Hee!");
})
.closest(".mx_EventTile")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
});
});
});

View file

@ -98,9 +98,11 @@ export function checkDeviceIsCrossSigned(): void {
}
/**
* Fill in the login form in element with the given creds
* Fill in the login form in element with the given creds.
*
* If a `securityKey` is given, verifies the new device using the key.
*/
export function logIntoElement(homeserverUrl: string, username: string, password: string) {
export function logIntoElement(homeserverUrl: string, username: string, password: string, securityKey?: string) {
cy.visit("/#/login");
// select homeserver
@ -114,6 +116,32 @@ export function logIntoElement(homeserverUrl: string, username: string, password
cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click();
// if a securityKey was given, verify the new device
if (securityKey !== undefined) {
cy.get(".mx_AuthPage").within(() => {
cy.findByRole("button", { name: "Verify with Security Key" }).click();
});
cy.get(".mx_Dialog").within(() => {
// Fill in the security key
cy.get('input[type="password"]').type(securityKey);
});
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
cy.findByRole("button", { name: "Done" }).click();
}
}
/**
* Queue up Cypress commands to log out of Element
*/
export function logOutOfElement() {
cy.findByRole("button", { name: "User menu" }).click();
cy.get(".mx_UserMenu_contextMenu").within(() => {
cy.findByRole("menuitem", { name: "Sign out" }).click();
});
cy.get(".mx_Dialog .mx_QuestionDialog").within(() => {
cy.findByRole("button", { name: "Sign out" }).click();
});
}
/**
@ -139,3 +167,36 @@ export function doTwoWaySasVerification(verifier: Verifier): void {
});
});
}
/**
* Queue up cypress commands to open the security settings and enable secure key backup.
*
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
*
* Stores the security key in `@securityKey`.
*/
export function enableKeyBackup() {
cy.openUserSettings("Security & Privacy");
cy.findByRole("button", { name: "Set up Secure Backup" }).click();
cy.get(".mx_Dialog").within(() => {
// Recovery key is selected by default
cy.findByRole("button", { name: "Continue", timeout: 60000 }).click();
// copy the text ourselves
cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey", { type: "static" });
downloadKey();
cy.findByText("Secure Backup successful").should("exist");
cy.findByRole("button", { name: "Done" }).click();
cy.findByText("Secure Backup successful").should("not.exist");
});
}
/**
* Queue up cypress commands to click on download button and continue
*/
export function downloadKey() {
// Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
cy.findByRole("button", { name: "Download" }).click();
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
}