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 signup = require('./tests/signup');
const join = require('./tests/join'); const join = require('./tests/join');
const sendMessage = require('./tests/send-message'); const sendMessage = require('./tests/send-message');
const acceptInvite = require('./tests/accept-invite');
const invite = require('./tests/invite');
const receiveMessage = require('./tests/receive-message'); const receiveMessage = require('./tests/receive-message');
const createRoom = require('./tests/create-room'); const createRoom = require('./tests/create-room');
const changeRoomSettings = require('./tests/room-settings'); const changeRoomSettings = require('./tests/room-settings');
const acceptServerNoticesInviteAndConsent = require('./tests/server-notices-consent'); 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) { module.exports = async function scenario(createSession) {
async function createUser(username) { async function createUser(username) {
const session = await createSession(username); const session = await createSession(username);
await signup(session, session.username, 'testtest'); await signup(session, session.username, 'testtest');
const noticesName = "Server Notices"; await acceptServerNoticesInviteAndConsent(session);
await acceptServerNoticesInviteAndConsent(session, noticesName);
return session; return session;
} }
@ -36,22 +40,46 @@ module.exports = async function scenario(createSession) {
const bob = await createUser("bob"); const bob = await createUser("bob");
await createDirectoryRoomAndTalk(alice, bob); await createDirectoryRoomAndTalk(alice, bob);
await createE2ERoomAndTalk(alice, bob);
} }
async function createDirectoryRoomAndTalk(alice, bob) { async function createDirectoryRoomAndTalk(alice, bob) {
console.log(" creating a public room and join through directory:"); console.log(" creating a public room and join through directory:");
const room = 'test'; const room = 'test';
const message = "hi Alice!";
await createRoom(alice, room); await createRoom(alice, room);
await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"}); await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests"});
await join(bob, room); await join(bob, room);
await sendMessage(bob, message); const bobMessage = "hi Alice!";
await receiveMessage(alice, {sender: "bob", body: message}); 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) { 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 changeRoomSettings(bob, {encryption: true});
await invite(bob, "@alice:localhost"); 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'); const assert = require('assert');
module.exports = async function createRoom(session, roomName) { module.exports = async function createRoom(session, roomName) {
session.log.step(`creates room ${roomName}`); session.log.step(`creates room "${roomName}"`);
//TODO: brittle selector //TODO: brittle selector
const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]'); const createRoomButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Create new room"]');
await createRoomButton.click(); 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'); const assert = require('assert');
module.exports = async function join(session, roomName) { module.exports = async function join(session, roomName) {
session.log.step(`joins room ${roomName}`); session.log.step(`joins room "${roomName}"`);
//TODO: brittle selector //TODO: brittle selector
const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]'); const directoryButton = await session.waitAndQuery('.mx_RoleButton[aria-label="Room directory"]');
await directoryButton.click(); await directoryButton.click();

View file

@ -16,26 +16,33 @@ limitations under the License.
const assert = require('assert'); const assert = require('assert');
async function getMessageFromTile(eventTile) {
}
module.exports = async function receiveMessage(session, message) { 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 // wait for a response to come in that contains the message
// crude, but effective // crude, but effective
await session.page.waitForResponse(async (response) => { await session.page.waitForResponse(async (response) => {
if (response.request().url().indexOf("/sync") === -1) {
return false;
}
const body = await response.text(); 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 // 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"); let lastTile = await session.query(".mx_EventTile_last");
const senderElement = await lastTile.$(".mx_SenderProfile_name"); const senderElement = await lastTile.$(".mx_SenderProfile_name");
const bodyElement = await lastTile.$(".mx_EventTile_body"); const bodyElement = await lastTile.$(".mx_EventTile_body");
const sender = await(await senderElement.getProperty("innerText")).jsonValue(); const sender = await(await senderElement.getProperty("innerText")).jsonValue();
const body = await(await bodyElement.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(body, message.body);
assert.equal(sender, message.sender); assert.equal(sender, message.sender);
session.log.done(); session.log.done();

View file

@ -15,6 +15,19 @@ limitations under the License.
*/ */
const assert = require('assert'); 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) { module.exports = async function changeRoomSettings(session, settings) {
session.log.startGroup(`changes the room settings`); session.log.startGroup(`changes the room settings`);
@ -31,13 +44,15 @@ module.exports = async function changeRoomSettings(session, settings) {
if (typeof settings.directory === "boolean") { if (typeof settings.directory === "boolean") {
session.log.step(`sets directory listing to ${settings.directory}`); session.log.step(`sets directory listing to ${settings.directory}`);
const checked = await session.getElementProperty(isDirectory, "checked"); await setCheckboxSetting(session, isDirectory, settings.directory);
assert.equal(typeof checked, "boolean"); }
if (checked !== settings.directory) {
await isDirectory.click(); if (typeof settings.encryption === "boolean") {
session.log.done(); session.log.step(`sets room e2e encryption to ${settings.encryption}`);
} else { const clicked = await setCheckboxSetting(session, e2eEncryptionCheck, settings.encryption);
session.log.done("already set"); // 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"); const saveButton = await session.query(".mx_RoomHeader_wrapper .mx_RoomHeader_textButton");
await saveButton.click(); await saveButton.click();
session.log.endGroup(); session.log.endGroup();
} }

View file

@ -18,8 +18,12 @@ const assert = require('assert');
module.exports = async function sendMessage(session, message) { module.exports = async function sendMessage(session, message) {
session.log.step(`writes "${message}" in room`); 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); await composer.type(message);
const text = await session.innerText(composer);
assert.equal(text.trim(), message.trim());
await composer.press("Enter"); await composer.press("Enter");
session.log.done(); session.log.done();
} }

View file

@ -15,26 +15,11 @@ limitations under the License.
*/ */
const assert = require('assert'); const assert = require('assert');
const acceptInvite = require("./accept-invite")
module.exports = async function acceptServerNoticesInviteAndConsent(session, noticesName) { module.exports = async function acceptServerNoticesInviteAndConsent(session) {
session.log.step(`accepts "${noticesName}" invite and accepting terms & conditions`); await acceptInvite(session, "Server Notices");
//TODO: brittle selector session.log.step(`accepts terms & conditions`);
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 consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000); const consentLink = await session.waitAndQuery(".mx_EventTile_body a", 1000);
const termsPagePromise = session.waitForNewPage(); const termsPagePromise = session.waitForNewPage();
await consentLink.click(); await consentLink.click();
const termsPage = await termsPagePromise; 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 riotserver = 'http://localhost:5000';
const noLogs = process.argv.indexOf("--no-logs") !== -1;
const debug = process.argv.indexOf("--debug") !== -1;
async function runTests() { async function runTests() {
let sessions = []; let sessions = [];
console.log("running tests ..."); console.log("running tests ...");
const options = {}; const options = {};
// options.headless = false; if (debug) {
// options.slowMo = 10;
options.headless = false;
}
if (process.env.CHROME_PATH) { if (process.env.CHROME_PATH) {
const path = 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)`); console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`);
@ -44,20 +50,28 @@ async function runTests() {
} catch(err) { } catch(err) {
failure = true; failure = true;
console.log('failure: ', err); console.log('failure: ', err);
for(let i = 0; i < sessions.length; ++i) { if (!noLogs) {
const session = sessions[i]; for(let i = 0; i < sessions.length; ++i) {
documentHtml = await session.page.content(); const session = sessions[i];
console.log(`---------------- START OF ${session.username} LOGS ----------------`); documentHtml = await session.page.content();
console.log('---------------- console.log output:'); console.log(`---------------- START OF ${session.username} LOGS ----------------`);
console.log(session.consoleLogs()); console.log('---------------- console.log output:');
console.log('---------------- network requests:'); console.log(session.consoleLogs());
console.log(session.networkLogs()); console.log('---------------- network requests:');
console.log('---------------- document html:'); console.log(session.networkLogs());
console.log(documentHtml); console.log('---------------- document html:');
console.log(`---------------- END OF ${session.username} LOGS ----------------`); 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())); await Promise.all(sessions.map((session) => session.close()));
if (failure) { if (failure) {