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:
parent
fca9f0e91d
commit
3818c1dc70
2 changed files with 284 additions and 75 deletions
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue