Merge pull request #6 from matrix-org/bwindels/e2erooms

Tests for creating and writing in a e2e encrypted room
This commit is contained in:
Bruno Windels 2018-08-14 12:43:06 +02:00 committed by GitHub
commit a54f13cd68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 303 additions and 58 deletions

View file

@ -15,20 +15,24 @@ limitations under the License.
*/
const {acceptDialog} = require('./tests/dialog');
const signup = require('./tests/signup');
const join = require('./tests/join');
const sendMessage = require('./tests/send-message');
const acceptInvite = require('./tests/accept-invite');
const invite = require('./tests/invite');
const receiveMessage = require('./tests/receive-message');
const createRoom = require('./tests/create-room');
const changeRoomSettings = require('./tests/room-settings');
const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent');
const getE2EDeviceFromSettings = require('./tests/e2e-device');
const verifyDeviceForUser = require("./tests/verify-device");
module.exports = async function scenario(createSession) {
async function createUser(username) {
const session = await createSession(username);
await signup(session, session.username, 'testtest');
const noticesName = "Server Notices";
await acceptServerNoticesInviteAndConsent(session, noticesName);
await acceptServerNoticesInviteAndConsent(session);
return session;
}
@ -36,22 +40,46 @@ module.exports = async function scenario(createSession) {
const bob = await createUser("bob");
await createDirectoryRoomAndTalk(alice, bob);
await createE2ERoomAndTalk(alice, bob);
}
async function createDirectoryRoomAndTalk(alice, bob) {
console.log(" creating a public room and join through directory:");
const room = 'test';
const message = "hi Alice!";
await createRoom(alice, room);
await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"});
await join(bob, room);
await sendMessage(bob, message);
await receiveMessage(alice, {sender: "bob", body: message});
}
const bobMessage = "hi Alice!";
await sendMessage(bob, bobMessage);
await receiveMessage(alice, {sender: "bob", body: bobMessage});
const aliceMessage = "hi Bob, welcome!"
await sendMessage(alice, aliceMessage);
await receiveMessage(bob, {sender: "alice", body: aliceMessage});
}
async function createE2ERoomAndTalk(alice, bob) {
await createRoom(bob, "secrets");
console.log(" creating an e2e encrypted room and join through invite:");
const room = "secrets";
await createRoom(bob, room);
await changeRoomSettings(bob, {encryption: true});
await invite(bob, "@alice:localhost");
await acceptInvite(alice, "secrets");
}
await acceptInvite(alice, room);
const bobDevice = await getE2EDeviceFromSettings(bob);
// wait some time for the encryption warning dialog
// to appear after closing the settings
await bob.delay(500);
await acceptDialog(bob, "encryption");
const aliceDevice = await getE2EDeviceFromSettings(alice);
// wait some time for the encryption warning dialog
// to appear after closing the settings
await alice.delay(500);
await acceptDialog(alice, "encryption");
await verifyDeviceForUser(bob, "alice", aliceDevice);
await verifyDeviceForUser(alice, "bob", bobDevice);
const aliceMessage = "Guess what I just heard?!"
await sendMessage(alice, aliceMessage);
await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true});
const bobMessage = "You've got to tell me!";
await sendMessage(bob, bobMessage);
await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true});
}

View file

@ -0,0 +1,41 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
const {acceptDialogMaybe} = require('./dialog');
module.exports = async function acceptInvite(session, name) {
session.log.step(`accepts "${name}" invite`);
//TODO: brittle selector
const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite', 1000);
const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => {
const text = await session.innerText(inviteHandle);
return {inviteHandle, text};
}));
const inviteHandle = invitesWithText.find(({inviteHandle, text}) => {
return text.trim() === name;
}).inviteHandle;
await inviteHandle.click();
const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child");
await acceptInvitationLink.click();
// accept e2e warning dialog
acceptDialogMaybe(session, "encryption");
session.log.done();
}

View file

@ -17,7 +17,7 @@ limitations under the License.
const assert = require('assert');
module.exports = async function createRoom(session, roomName) {
session.log.step(`creates room ${roomName}`);
session.log.step(`creates room "${roomName}"`);
//TODO: brittle selector
const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]');
await createRoomButton.click();

47
src/tests/dialog.js Normal file
View file

@ -0,0 +1,47 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
async function acceptDialog(session, expectedContent) {
const foundDialog = await acceptDialogMaybe(session, expectedContent);
if (!foundDialog) {
throw new Error("could not find a dialog");
}
}
async function acceptDialogMaybe(session, expectedContent) {
let dialog = null;
try {
dialog = await session.waitAndQuery(".mx_QuestionDialog", 100);
} catch(err) {
return false;
}
if (expectedContent) {
const contentElement = await dialog.$(".mx_Dialog_content");
const content = await (await contentElement.getProperty("innerText")).jsonValue();
assert.ok(content.indexOf(expectedContent) !== -1);
}
const primaryButton = await dialog.$(".mx_Dialog_primary");
await primaryButton.click();
return true;
}
module.exports = {
acceptDialog,
acceptDialogMaybe,
};

31
src/tests/e2e-device.js Normal file
View file

@ -0,0 +1,31 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
module.exports = async function getE2EDeviceFromSettings(session) {
session.log.step(`gets e2e device/key from settings`);
const settingsButton = await session.query('.mx_BottomLeftMenu_settings');
await settingsButton.click();
const deviceAndKey = await session.waitAndQueryAll(".mx_UserSettings_section.mx_UserSettings_cryptoSection code");
assert.equal(deviceAndKey.length, 2);
const id = await (await deviceAndKey[0].getProperty("innerText")).jsonValue();
const key = await (await deviceAndKey[1].getProperty("innerText")).jsonValue();
const closeButton = await session.query(".mx_RoomHeader_cancelButton");
await closeButton.click();
session.log.done();
return {id, key};
}

30
src/tests/invite.js Normal file
View file

@ -0,0 +1,30 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
module.exports = async function invite(session, userId) {
session.log.step(`invites "${userId}" to room`);
await session.delay(200);
const inviteButton = await session.waitAndQuery(".mx_RightPanel_invite");
await inviteButton.click();
const inviteTextArea = await session.waitAndQuery(".mx_ChatInviteDialog textarea");
await inviteTextArea.type(userId);
await inviteTextArea.press("Enter");
const confirmButton = await session.query(".mx_Dialog_primary");
await confirmButton.click();
session.log.done();
}

View file

@ -17,7 +17,7 @@ limitations under the License.
const assert = require('assert');
module.exports = async function join(session, roomName) {
session.log.step(`joins room ${roomName}`);
session.log.step(`joins room "${roomName}"`);
//TODO: brittle selector
const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]');
await directoryButton.click();

View file

@ -16,26 +16,33 @@ limitations under the License.
const assert = require('assert');
async function getMessageFromTile(eventTile) {
}
module.exports = async function receiveMessage(session, message) {
session.log.step(`receives message "${message.body}" from ${message.sender} in room`);
session.log.step(`receives message "${message.body}" from ${message.sender}`);
// wait for a response to come in that contains the message
// crude, but effective
await session.page.waitForResponse(async (response) => {
if (response.request().url().indexOf("/sync") === -1) {
return false;
}
const body = await response.text();
return body.indexOf(message.body) !== -1;
if (message.encrypted) {
return body.indexOf(message.sender) !== -1 &&
body.indexOf("m.room.encrypted") !== -1;
} else {
return body.indexOf(message.body) !== -1;
}
});
// wait a bit for the incoming event to be rendered
await session.delay(100);
await session.delay(300);
let lastTile = await session.query(".mx_EventTile_last");
const senderElement = await lastTile.$(".mx_SenderProfile_name");
const bodyElement = await lastTile.$(".mx_EventTile_body");
const sender = await(await senderElement.getProperty("innerText")).jsonValue();
const body = await(await bodyElement.getProperty("innerText")).jsonValue();
if (message.encrypted) {
const e2eIcon = await lastTile.$(".mx_EventTile_e2eIcon");
assert.ok(e2eIcon);
}
assert.equal(body, message.body);
assert.equal(sender, message.sender);
session.log.done();

View file

@ -15,6 +15,19 @@ limitations under the License.
*/
const assert = require('assert');
const {acceptDialog} = require('./dialog');
async function setCheckboxSetting(session, checkbox, enabled) {
const checked = await session.getElementProperty(checkbox, "checked");
assert.equal(typeof checked, "boolean");
if (checked !== enabled) {
await checkbox.click();
session.log.done();
return true;
} else {
session.log.done("already set");
}
}
module.exports = async function changeRoomSettings(session, settings) {
session.log.startGroup(`changes the room settings`);
@ -31,13 +44,15 @@ module.exports = async function changeRoomSettings(session, settings) {
if (typeof settings.directory === "boolean") {
session.log.step(`sets directory listing to ${settings.directory}`);
const checked = await session.getElementProperty(isDirectory, "checked");
assert.equal(typeof checked, "boolean");
if (checked !== settings.directory) {
await isDirectory.click();
session.log.done();
} else {
session.log.done("already set");
await setCheckboxSetting(session, isDirectory, settings.directory);
}
if (typeof settings.encryption === "boolean") {
session.log.step(`sets room e2e encryption to ${settings.encryption}`);
const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption);
// if enabling, accept beta warning dialog
if (clicked && settings.encryption) {
await acceptDialog(session, "encryption");
}
}
@ -63,5 +78,6 @@ module.exports = async function changeRoomSettings(session, settings) {
const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton");
await saveButton.click();
session.log.endGroup();
}

View file

@ -18,8 +18,12 @@ const assert = require('assert');
module.exports = async function sendMessage(session, message) {
session.log.step(`writes "${message}" in room`);
const composer = await session.waitAndQuery('.mx_MessageComposer');
// this selector needs to be the element that has contenteditable=true,
// not any if its parents, otherwise it behaves flaky at best.
const composer = await session.waitAndQuery('.mx_MessageComposer_editor');
await composer.type(message);
const text = await session.innerText(composer);
assert.equal(text.trim(), message.trim());
await composer.press("Enter");
session.log.done();
}

View file

@ -15,26 +15,11 @@ limitations under the License.
*/
const assert = require('assert');
module.exports = async function acceptServerNoticesInviteAndConsent(session, noticesName) {
session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`);
//TODO: brittle selector
const invitesHandles = await session.waitAndQueryAll('.mx_RoomTile_name.mx_RoomTile_invite');
const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => {
const text = await session.innerText(inviteHandle);
return {inviteHandle, text};
}));
const inviteHandle = invitesWithText.find(({inviteHandle, text}) => {
return text.trim() === noticesName;
}).inviteHandle;
await inviteHandle.click();
const acceptInvitationLink = await session.waitAndQuery(".mx_RoomPreviewBar_join_text a:first-child");
await acceptInvitationLink.click();
const acceptInvite = require("./accept-invite")
module.exports = async function acceptServerNoticesInviteAndConsent(session) {
await acceptInvite(session, "Server Notices");
session.log.step(`accepts terms & conditions`);
const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000);
const termsPagePromise = session.waitForNewPage();
await consentLink.click();
const termsPage = await termsPagePromise;

View file

@ -0,0 +1,42 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const assert = require('assert');
module.exports = async function verifyDeviceForUser(session, name, expectedDevice) {
session.log.step(`verifies e2e device for ${name}`);
const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name");
const membersAndNames = await Promise.all(memberNameElements.map(async (el) => {
return [el, await session.innerText(el)];
}));
const matchingMember = membersAndNames.filter(([el, text]) => {
return text === name;
}).map(([el]) => el)[0];
await matchingMember.click();
const firstVerifyButton = await session.waitAndQuery(".mx_MemberDeviceInfo_verify");
await firstVerifyButton.click();
const dialogCodeFields = await session.waitAndQueryAll(".mx_QuestionDialog code");
assert.equal(dialogCodeFields.length, 2);
const deviceId = await session.innerText(dialogCodeFields[0]);
const deviceKey = await session.innerText(dialogCodeFields[1]);
assert.equal(expectedDevice.id, deviceId);
assert.equal(expectedDevice.key, deviceKey);
const confirmButton = await session.query(".mx_Dialog_primary");
await confirmButton.click();
const closeMemberInfo = await session.query(".mx_MemberInfo_cancel");
await closeMemberInfo.click();
session.log.done();
}

View file

@ -20,12 +20,18 @@ const scenario = require('./src/scenario');
const riotserver = 'http://localhost:5000';
const noLogs = process.argv.indexOf("--no-logs") !== -1;
const debug = process.argv.indexOf("--debug") !== -1;
async function runTests() {
let sessions = [];
console.log("running tests ...");
const options = {};
// options.headless = false;
if (debug) {
// options.slowMo = 10;
options.headless = false;
}
if (process.env.CHROME_PATH) {
const path = process.env.CHROME_PATH;
console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`);
@ -44,20 +50,28 @@ async function runTests() {
} catch(err) {
failure = true;
console.log('failure: ', err);
for(let i = 0; i < sessions.length; ++i) {
const session = sessions[i];
documentHtml = await session.page.content();
console.log(`---------------- START OF ${session.username} LOGS ----------------`);
console.log('---------------- console.log output:');
console.log(session.consoleLogs());
console.log('---------------- network requests:');
console.log(session.networkLogs());
console.log('---------------- document html:');
console.log(documentHtml);
console.log(`---------------- END OF ${session.username} LOGS ----------------`);
if (!noLogs) {
for(let i = 0; i < sessions.length; ++i) {
const session = sessions[i];
documentHtml = await session.page.content();
console.log(`---------------- START OF ${session.username} LOGS ----------------`);
console.log('---------------- console.log output:');
console.log(session.consoleLogs());
console.log('---------------- network requests:');
console.log(session.networkLogs());
console.log('---------------- document html:');
console.log(documentHtml);
console.log(`---------------- END OF ${session.username} LOGS ----------------`);
}
}
}
// wait 5 minutes on failure if not running headless
// to inspect what went wrong
if (failure && options.headless === false) {
await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
}
await Promise.all(sessions.map((session) => session.close()));
if (failure) {