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 type { CypressBot } from "../../support/bot";
import { HomeserverInstance } from "../../plugins/utils/homeserver"; import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login"; import { UserCredentials } from "../../support/login";
import { doTwoWaySasVerification, waitForVerificationRequest } from "./utils"; import {
doTwoWaySasVerification,
downloadKey,
enableKeyBackup,
logIntoElement,
logOutOfElement,
waitForVerificationRequest,
} from "./utils";
import { skipIfRustCrypto } from "../../support/util"; import { skipIfRustCrypto } from "../../support/util";
interface CryptoTestContext extends Mocha.Context { interface CryptoTestContext extends Mocha.Context {
@ -129,19 +136,26 @@ const verify = function (this: CryptoTestContext) {
describe("Cryptography", function () { describe("Cryptography", function () {
let aliceCredentials: UserCredentials; let aliceCredentials: UserCredentials;
let homeserver: HomeserverInstance;
let bob: CypressBot;
beforeEach(function () { beforeEach(function () {
cy.startHomeserver("default") cy.startHomeserver("default")
.as("homeserver") .as("homeserver")
.then((homeserver: HomeserverInstance) => { .then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => { cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
aliceCredentials = credentials; aliceCredentials = credentials;
}); });
cy.getBot(homeserver, { return cy.getBot(homeserver, {
displayName: "Bob", displayName: "Bob",
autoAcceptInvites: false, autoAcceptInvites: false,
userIdPrefix: "bob_", 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", () => { it("by recovery code", () => {
skipIfRustCrypto(); skipIfRustCrypto();
@ -294,53 +299,217 @@ describe("Cryptography", function () {
verify.call(this); verify.call(this);
}); });
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) { describe("event shields", () => {
skipIfRustCrypto(); let testRoomId: string;
cy.bootstrapCrossSigning(aliceCredentials);
// bob has a second, not cross-signed, device beforeEach(() => {
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice"); 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 // enable encryption
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }) cy.getClient().then((cli) => {
.as("testRoomId") cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" });
.then((roomId) => { });
cy.log(`Created test room ${roomId}`);
cy.visit(`/#/room/${roomId}`);
// enable encryption // wait for Bob to join the room, otherwise our attempt to open his user details may race
cy.getClient().then((cli) => { // with his join.
cli.sendStateEvent(roomId, "m.room.encryption", { algorithm: "m.megolm.v1.aes-sha2" }); 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 it("should show the correct shield on e2e events", function (this: CryptoTestContext) {
// with his join. skipIfRustCrypto();
cy.findByText("Bob joined the room").should("exist");
// 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 // 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 // the message should appear, decrypted, with no warning, but also no "verified"
cy.get(".mx_EventTile_last .mx_EventTile_body") cy.get(".mx_EventTile_last")
.within(() => { .should("contain", "test encrypted 1")
cy.findByText("Hoo!"); // no e2e icon
}) .should("not.have.descendants", ".mx_EventTile_e2eIcon");
.closest(".mx_EventTile")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
// bob sends an edit to the first message with his unverified device /* Now verify Bob */
cy.get<MatrixClient>("@bobSecondDevice").then((bobSecondDevice) => { 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) => { cy.get<ISendEventResponse>("@testEvent").then((testEvent) => {
bobSecondDevice.sendMessage(roomId, { this.bob.sendMessage(roomId, {
"m.new_content": { "m.new_content": {
msgtype: "m.text", msgtype: "m.text",
body: "Haa!", body: "Hee!",
}, },
"m.relates_to": { "m.relates_to": {
rel_type: "m.replace", 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"); cy.visit("/#/login");
// select homeserver // select homeserver
@ -114,6 +116,32 @@ export function logIntoElement(homeserverUrl: string, username: string, password
cy.findByRole("textbox", { name: "Username" }).type(username); cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password); cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click(); 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();
}