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 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");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue