diff --git a/test/end-to-end-tests/.gitignore b/test/end-to-end-tests/.gitignore new file mode 100644 index 0000000000..afca1ddcb3 --- /dev/null +++ b/test/end-to-end-tests/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.png +riot/env diff --git a/test/end-to-end-tests/README.md b/test/end-to-end-tests/README.md new file mode 100644 index 0000000000..81655000a0 --- /dev/null +++ b/test/end-to-end-tests/README.md @@ -0,0 +1,43 @@ +# Matrix React Web App End-to-End tests + +This repository contains tests for the matrix-react-sdk web app. The tests fire up a headless chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser. + +## Setup + +Run `./install.sh`. This will: + - install synapse, fetches the master branch at the moment. If anything fails here, please refer to the synapse README to see if you're missing one of the prerequisites. + - install riot, this fetches the master branch at the moment. + - install dependencies (will download copy of chrome) + +## Running the tests + +Run tests with `./run.sh`. + +### Debug tests locally. + +`./run.sh` will run the tests against the riot copy present in `riot/riot-web` served by a static python http server. You can symlink your `riot-web` develop copy here but that doesn't work well with webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local webpack server. + +``` +./synapse/stop.sh && \ +./synapse/start.sh && \ +node start.js +``` +It's important to always stop and start synapse before each run of the tests to clear the in-memory sqlite database it uses, as the tests assume a blank slate. + +start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally: + + - `--riot-url ` don't use the riot copy and static server provided by the tests, but use a running server like the webpack watch server to run the tests against. Make sure to have the following local config: + - `welcomeUserId` disabled as the tests assume there is no riot-bot currently. + - `--slow-mo` type at a human speed, useful with `--windowed`. + - `--throttle-cpu ` throttle cpu in the browser by the given factor. Useful to reproduce failures because of insufficient timeouts happening on the slower CI server. + - `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though. + - `--dev-tools` open the devtools in the browser window, only applies if `--windowed` is set as well. + +Developer Guide +=============== + +Please follow the standard Matrix contributor's guide: +https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst + +Please follow the Matrix JS/React code style as per: +https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md diff --git a/test/end-to-end-tests/TODO.md b/test/end-to-end-tests/TODO.md new file mode 100644 index 0000000000..f5ccebcf77 --- /dev/null +++ b/test/end-to-end-tests/TODO.md @@ -0,0 +1,8 @@ + + - join a peekable room by directory + - join a peekable room by invite + - join a non-peekable room by directory + - join a non-peekable room by invite + - leave a room and check we move to the "next" room + - get kicked from a room and check that we show the correct message + - get banned " diff --git a/test/end-to-end-tests/install.sh b/test/end-to-end-tests/install.sh new file mode 100755 index 0000000000..e1fed144ce --- /dev/null +++ b/test/end-to-end-tests/install.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed +set -e +./synapse/install.sh +./riot/install.sh +yarn install diff --git a/test/end-to-end-tests/package.json b/test/end-to-end-tests/package.json new file mode 100644 index 0000000000..8372039258 --- /dev/null +++ b/test/end-to-end-tests/package.json @@ -0,0 +1,19 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "cheerio": "^1.0.0-rc.2", + "commander": "^2.19.0", + "puppeteer": "^1.14.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "uuid": "^3.3.2" + } +} diff --git a/test/end-to-end-tests/riot/.gitignore b/test/end-to-end-tests/riot/.gitignore new file mode 100644 index 0000000000..0f07d8e498 --- /dev/null +++ b/test/end-to-end-tests/riot/.gitignore @@ -0,0 +1,2 @@ +riot-web +riot.pid \ No newline at end of file diff --git a/test/end-to-end-tests/riot/config-template/config.json b/test/end-to-end-tests/riot/config-template/config.json new file mode 100644 index 0000000000..6277e567fc --- /dev/null +++ b/test/end-to-end-tests/riot/config-template/config.json @@ -0,0 +1,33 @@ +{ + "default_hs_url": "http://localhost:5005", + "default_is_url": "https://vector.im", + "disable_custom_urls": false, + "disable_guests": false, + "disable_login_language_selector": false, + "disable_3pid_login": false, + "brand": "Riot", + "integrations_ui_url": "https://scalar.vector.im/", + "integrations_rest_url": "https://scalar.vector.im/api", + "bug_report_endpoint_url": "https://riot.im/bugreports/submit", + "features": { + "feature_groups": "labs", + "feature_pinning": "labs" + }, + "default_federate": true, + "welcomePageUrl": "home.html", + "default_theme": "light", + "roomDirectory": { + "servers": [ + "localhost:5005" + ] + }, + "piwik": { + "url": "https://piwik.riot.im/", + "whitelistedHSUrls": ["http://localhost:5005"], + "whitelistedISUrls": ["https://vector.im", "https://matrix.org"], + "siteId": 1 + }, + "enable_presence_by_hs_url": { + "https://matrix.org": false + } +} diff --git a/test/end-to-end-tests/riot/install.sh b/test/end-to-end-tests/riot/install.sh new file mode 100755 index 0000000000..8a942a05ea --- /dev/null +++ b/test/end-to-end-tests/riot/install.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e +RIOT_BRANCH=develop + +BASE_DIR=$(cd $(dirname $0) && pwd) +cd $BASE_DIR +# Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer +# but with support for multiple threads) into a virtualenv. +( + virtualenv -p python3 env + source env/bin/activate + + # Having been bitten by pip SSL fail too many times, I don't trust the existing pip + # to be able to --upgrade itself, so grab a new one fresh from source. + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py + python get-pip.py + rm get-pip.py + + pip install ComplexHttpServer + + deactivate +) + +if [ -d $BASE_DIR/riot-web ]; then + echo "riot is already installed" + exit +fi + +curl -L https://github.com/vector-im/riot-web/archive/${RIOT_BRANCH}.zip --output riot.zip +unzip -q riot.zip +rm riot.zip +mv riot-web-${RIOT_BRANCH} riot-web +cd riot-web +yarn install +yarn run build diff --git a/test/end-to-end-tests/riot/start.sh b/test/end-to-end-tests/riot/start.sh new file mode 100755 index 0000000000..be226ed257 --- /dev/null +++ b/test/end-to-end-tests/riot/start.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -e + +PORT=5000 +BASE_DIR=$(cd $(dirname $0) && pwd) +PIDFILE=$BASE_DIR/riot.pid +CONFIG_BACKUP=config.e2etests_backup.json + +if [ -f $PIDFILE ]; then + exit +fi + +cd $BASE_DIR/ +echo -n "starting riot on http://localhost:$PORT ... " +pushd riot-web/webapp/ > /dev/null + +# backup config file before we copy template +if [ -f config.json ]; then + mv config.json $CONFIG_BACKUP +fi +cp $BASE_DIR/config-template/config.json . + +LOGFILE=$(mktemp) +# run web server in the background, showing output on error +( + source $BASE_DIR/env/bin/activate + python -m ComplexHTTPServer $PORT > $LOGFILE 2>&1 & + PID=$! + echo $PID > $PIDFILE + # wait so subshell does not exit + # otherwise sleep below would not work + wait $PID; RESULT=$? + + # NOT expected SIGTERM (128 + 15) + # from stop.sh? + if [ $RESULT -ne 143 ]; then + echo "failed" + cat $LOGFILE + rm $PIDFILE 2> /dev/null + fi + rm $LOGFILE + exit $RESULT +)& +# to be able to return the exit code for immediate errors (like address already in use) +# we wait for a short amount of time in the background and exit when the first +# child process exits +sleep 0.5 & +# wait the first child process to exit (python or sleep) +wait -n; RESULT=$? +# return exit code of first child to exit +if [ $RESULT -eq 0 ]; then + echo "running" +fi +exit $RESULT diff --git a/test/end-to-end-tests/riot/stop.sh b/test/end-to-end-tests/riot/stop.sh new file mode 100755 index 0000000000..eb99fa11cc --- /dev/null +++ b/test/end-to-end-tests/riot/stop.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) +PIDFILE=riot.pid +CONFIG_BACKUP=config.e2etests_backup.json + +cd $BASE_DIR + +if [ -f $PIDFILE ]; then + echo "stopping riot server ..." + PID=$(cat $PIDFILE) + rm $PIDFILE + kill $PID + + # revert config file + cd riot-web/webapp + rm config.json + if [ -f $CONFIG_BACKUP ]; then + mv $CONFIG_BACKUP config.json + fi +fi diff --git a/test/end-to-end-tests/run.sh b/test/end-to-end-tests/run.sh new file mode 100755 index 0000000000..0e03b733ce --- /dev/null +++ b/test/end-to-end-tests/run.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +stop_servers() { + ./riot/stop.sh + ./synapse/stop.sh +} + +handle_error() { + EXIT_CODE=$? + stop_servers + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +./synapse/start.sh +./riot/start.sh +node start.js $@ +stop_servers diff --git a/test/end-to-end-tests/src/logbuffer.js b/test/end-to-end-tests/src/logbuffer.js new file mode 100644 index 0000000000..8bf6285e25 --- /dev/null +++ b/test/end-to-end-tests/src/logbuffer.js @@ -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. +*/ + +module.exports = class LogBuffer { + constructor(page, eventName, eventMapper, reduceAsync=false, initialValue = "") { + this.buffer = initialValue; + page.on(eventName, (arg) => { + const result = eventMapper(arg); + if (reduceAsync) { + result.then((r) => this.buffer += r); + } + else { + this.buffer += result; + } + }); + } +} diff --git a/test/end-to-end-tests/src/logger.js b/test/end-to-end-tests/src/logger.js new file mode 100644 index 0000000000..be3ebde75b --- /dev/null +++ b/test/end-to-end-tests/src/logger.js @@ -0,0 +1,62 @@ +/* +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. +*/ + +module.exports = class Logger { + constructor(username) { + this.indent = 0; + this.username = username; + this.muted = false; + } + + startGroup(description) { + if (!this.muted) { + const indent = " ".repeat(this.indent * 2); + console.log(`${indent} * ${this.username} ${description}:`); + } + this.indent += 1; + return this; + } + + endGroup() { + this.indent -= 1; + return this; + } + + step(description) { + if (!this.muted) { + const indent = " ".repeat(this.indent * 2); + process.stdout.write(`${indent} * ${this.username} ${description} ... `); + } + return this; + } + + done(status = "done") { + if (!this.muted) { + process.stdout.write(status + "\n"); + } + return this; + } + + mute() { + this.muted = true; + return this; + } + + unmute() { + this.muted = false; + return this; + } +} diff --git a/test/end-to-end-tests/src/rest/consent.js b/test/end-to-end-tests/src/rest/consent.js new file mode 100644 index 0000000000..1e36f541a3 --- /dev/null +++ b/test/end-to-end-tests/src/rest/consent.js @@ -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 request = require('request-promise-native'); +const cheerio = require('cheerio'); +const url = require("url"); + +module.exports.approveConsent = async function(consentUrl) { + const body = await request.get(consentUrl); + const doc = cheerio.load(body); + const v = doc("input[name=v]").val(); + const u = doc("input[name=u]").val(); + const h = doc("input[name=h]").val(); + const formAction = doc("form").attr("action"); + const absAction = url.resolve(consentUrl, formAction); + await request.post(absAction).form({v, u, h}); +}; diff --git a/test/end-to-end-tests/src/rest/creator.js b/test/end-to-end-tests/src/rest/creator.js new file mode 100644 index 0000000000..31c352b31a --- /dev/null +++ b/test/end-to-end-tests/src/rest/creator.js @@ -0,0 +1,91 @@ +/* +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 {exec} = require('child_process'); +const request = require('request-promise-native'); +const RestSession = require('./session'); +const RestMultiSession = require('./multi'); + +function execAsync(command, options) { + return new Promise((resolve, reject) => { + exec(command, options, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve({stdout, stderr}); + } + }); + }); +} + +module.exports = class RestSessionCreator { + constructor(synapseSubdir, hsUrl, cwd) { + this.synapseSubdir = synapseSubdir; + this.hsUrl = hsUrl; + this.cwd = cwd; + } + + async createSessionRange(usernames, password, groupName) { + const sessionPromises = usernames.map((username) => this.createSession(username, password)); + const sessions = await Promise.all(sessionPromises); + return new RestMultiSession(sessions, groupName); + } + + async createSession(username, password) { + await this._register(username, password); + console.log(` * created REST user ${username} ... done`); + const authResult = await this._authenticate(username, password); + return new RestSession(authResult); + } + + async _register(username, password) { + const registerArgs = [ + '-c homeserver.yaml', + `-u ${username}`, + `-p ${password}`, + '--no-admin', + this.hsUrl + ]; + const registerCmd = `./register_new_matrix_user ${registerArgs.join(' ')}`; + const allCmds = [ + `cd ${this.synapseSubdir}`, + ". ./activate", + registerCmd + ].join(' && '); + + await execAsync(allCmds, {cwd: this.cwd, encoding: 'utf-8'}); + } + + async _authenticate(username, password) { + const requestBody = { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username + }, + "password": password + }; + const url = `${this.hsUrl}/_matrix/client/r0/login`; + const responseBody = await request.post({url, json: true, body: requestBody}); + return { + accessToken: responseBody.access_token, + homeServer: responseBody.home_server, + userId: responseBody.user_id, + deviceId: responseBody.device_id, + hsUrl: this.hsUrl, + }; + } +} diff --git a/test/end-to-end-tests/src/rest/multi.js b/test/end-to-end-tests/src/rest/multi.js new file mode 100644 index 0000000000..b930a27c1e --- /dev/null +++ b/test/end-to-end-tests/src/rest/multi.js @@ -0,0 +1,95 @@ +/* +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 request = require('request-promise-native'); +const RestRoom = require('./room'); +const {approveConsent} = require('./consent'); +const Logger = require('../logger'); + +module.exports = class RestMultiSession { + constructor(sessions, groupName) { + this.log = new Logger(groupName); + this.sessions = sessions; + } + + slice(groupName, start, end) { + return new RestMultiSession(this.sessions.slice(start, end), groupName); + } + + pop(userName) { + const idx = this.sessions.findIndex((s) => s.userName() === userName); + if(idx === -1) { + throw new Error(`user ${userName} not found`); + } + const session = this.sessions.splice(idx, 1)[0]; + return session; + } + + async setDisplayName(fn) { + this.log.step("set their display name") + await Promise.all(this.sessions.map(async (s) => { + s.log.mute(); + await s.setDisplayName(fn(s)); + s.log.unmute(); + })); + this.log.done(); + } + + async join(roomIdOrAlias) { + this.log.step(`join ${roomIdOrAlias}`) + const rooms = await Promise.all(this.sessions.map(async (s) => { + s.log.mute(); + const room = await s.join(roomIdOrAlias); + s.log.unmute(); + return room; + })); + this.log.done(); + return new RestMultiRoom(rooms, roomIdOrAlias, this.log); + } + + room(roomIdOrAlias) { + const rooms = this.sessions.map(s => s.room(roomIdOrAlias)); + return new RestMultiRoom(rooms, roomIdOrAlias, this.log); + } +} + +class RestMultiRoom { + constructor(rooms, roomIdOrAlias, log) { + this.rooms = rooms; + this.roomIdOrAlias = roomIdOrAlias; + this.log = log; + } + + async talk(message) { + this.log.step(`say "${message}" in ${this.roomIdOrAlias}`) + await Promise.all(this.rooms.map(async (r) => { + r.log.mute(); + await r.talk(message); + r.log.unmute(); + })); + this.log.done(); + } + + async leave() { + this.log.step(`leave ${this.roomIdOrAlias}`) + await Promise.all(this.rooms.map(async (r) => { + r.log.mute(); + await r.leave(); + r.log.unmute(); + })); + this.log.done(); + } +} diff --git a/test/end-to-end-tests/src/rest/room.js b/test/end-to-end-tests/src/rest/room.js new file mode 100644 index 0000000000..a7f40af594 --- /dev/null +++ b/test/end-to-end-tests/src/rest/room.js @@ -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 uuidv4 = require('uuid/v4'); + +/* no pun intented */ +module.exports = class RestRoom { + constructor(session, roomId, log) { + this.session = session; + this._roomId = roomId; + this.log = log; + } + + async talk(message) { + this.log.step(`says "${message}" in ${this._roomId}`) + const txId = uuidv4(); + await this.session._put(`/rooms/${this._roomId}/send/m.room.message/${txId}`, { + "msgtype": "m.text", + "body": message + }); + this.log.done(); + return txId; + } + + async leave() { + this.log.step(`leaves ${this._roomId}`) + await this.session._post(`/rooms/${this._roomId}/leave`); + this.log.done(); + } + + roomId() { + return this._roomId; + } +} diff --git a/test/end-to-end-tests/src/rest/session.js b/test/end-to-end-tests/src/rest/session.js new file mode 100644 index 0000000000..de05cd4b5c --- /dev/null +++ b/test/end-to-end-tests/src/rest/session.js @@ -0,0 +1,127 @@ +/* +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 request = require('request-promise-native'); +const Logger = require('../logger'); +const RestRoom = require('./room'); +const {approveConsent} = require('./consent'); + +module.exports = class RestSession { + constructor(credentials) { + this.log = new Logger(credentials.userId); + this._credentials = credentials; + this._displayName = null; + this._rooms = {}; + } + + userId() { + return this._credentials.userId; + } + + userName() { + return this._credentials.userId.split(":")[0].substr(1); + } + + displayName() { + return this._displayName; + } + + async setDisplayName(displayName) { + this.log.step(`sets their display name to ${displayName}`); + this._displayName = displayName; + await this._put(`/profile/${this._credentials.userId}/displayname`, { + displayname: displayName + }); + this.log.done(); + } + + async join(roomIdOrAlias) { + this.log.step(`joins ${roomIdOrAlias}`); + const {room_id} = await this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`); + this.log.done(); + const room = new RestRoom(this, room_id, this.log); + this._rooms[room_id] = room; + this._rooms[roomIdOrAlias] = room; + return room; + } + + room(roomIdOrAlias) { + if (this._rooms.hasOwnProperty(roomIdOrAlias)) { + return this._rooms[roomIdOrAlias]; + } else { + throw new Error(`${this._credentials.userId} is not in ${roomIdOrAlias}`); + } + } + + async createRoom(name, options) { + this.log.step(`creates room ${name}`); + const body = { + name, + }; + if (options.invite) { + body.invite = options.invite; + } + if (options.public) { + body.visibility = "public"; + } else { + body.visibility = "private"; + } + if (options.dm) { + body.is_direct = true; + } + if (options.topic) { + body.topic = options.topic; + } + + const {room_id} = await this._post(`/createRoom`, body); + this.log.done(); + return new RestRoom(this, room_id, this.log); + } + + _post(csApiPath, body) { + return this._request("POST", csApiPath, body); + } + + _put(csApiPath, body) { + return this._request("PUT", csApiPath, body); + } + + async _request(method, csApiPath, body) { + try { + const responseBody = await request({ + url: `${this._credentials.hsUrl}/_matrix/client/r0${csApiPath}`, + method, + headers: { + "Authorization": `Bearer ${this._credentials.accessToken}` + }, + json: true, + body + }); + return responseBody; + + } catch(err) { + const responseBody = err.response.body; + if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') { + await approveConsent(responseBody.consent_uri); + return this._request(method, csApiPath, body); + } else if(responseBody && responseBody.error) { + throw new Error(`${method} ${csApiPath}: ${responseBody.error}`); + } else { + throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`); + } + } + } +} diff --git a/test/end-to-end-tests/src/scenario.js b/test/end-to-end-tests/src/scenario.js new file mode 100644 index 0000000000..1ad177a4f5 --- /dev/null +++ b/test/end-to-end-tests/src/scenario.js @@ -0,0 +1,52 @@ +/* +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 {range} = require('./util'); +const signup = require('./usecases/signup'); +const roomDirectoryScenarios = require('./scenarios/directory'); +const lazyLoadingScenarios = require('./scenarios/lazy-loading'); +const e2eEncryptionScenarios = require('./scenarios/e2e-encryption'); + +module.exports = async function scenario(createSession, restCreator) { + let firstUser = true; + async function createUser(username) { + const session = await createSession(username); + if (firstUser) { + // only show browser version for first browser opened + console.log(`running tests on ${await session.browser.version()} ...`); + firstUser = false; + } + await signup(session, session.username, 'testsarefun!!!', session.hsUrl); + return session; + } + + const alice = await createUser("alice"); + const bob = await createUser("bob"); + + await roomDirectoryScenarios(alice, bob); + await e2eEncryptionScenarios(alice, bob); + console.log("create REST users:"); + const charlies = await createRestUsers(restCreator); + await lazyLoadingScenarios(alice, bob, charlies); +} + +async function createRestUsers(restCreator) { + const usernames = range(1, 10).map((i) => `charly-${i}`); + const charlies = await restCreator.createSessionRange(usernames, "testtest", "charly-1..10"); + await charlies.setDisplayName((s) => `Charly #${s.userName().split('-')[1]}`); + return charlies; +} diff --git a/test/end-to-end-tests/src/scenarios/README.md b/test/end-to-end-tests/src/scenarios/README.md new file mode 100644 index 0000000000..4eabc8f9ef --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/README.md @@ -0,0 +1 @@ +scenarios contains the high-level playbook for the test suite diff --git a/test/end-to-end-tests/src/scenarios/directory.js b/test/end-to-end-tests/src/scenarios/directory.js new file mode 100644 index 0000000000..582b6867b2 --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/directory.js @@ -0,0 +1,36 @@ +/* +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 join = require('../usecases/join'); +const sendMessage = require('../usecases/send-message'); +const {receiveMessage} = require('../usecases/timeline'); +const {createRoom} = require('../usecases/create-room'); +const changeRoomSettings = require('../usecases/room-settings'); + +module.exports = async function roomDirectoryScenarios(alice, bob) { + console.log(" creating a public room and join through directory:"); + const room = 'test'; + await createRoom(alice, room); + await changeRoomSettings(alice, {directory: true, visibility: "public_no_guests", alias: "#test"}); + await join(bob, room); //looks up room in directory + 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}); +} diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js new file mode 100644 index 0000000000..29b97f2047 --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -0,0 +1,52 @@ +/* +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 {delay} = require('../util'); +const {acceptDialogMaybe} = require('../usecases/dialog'); +const join = require('../usecases/join'); +const sendMessage = require('../usecases/send-message'); +const acceptInvite = require('../usecases/accept-invite'); +const invite = require('../usecases/invite'); +const {receiveMessage} = require('../usecases/timeline'); +const {createRoom} = require('../usecases/create-room'); +const changeRoomSettings = require('../usecases/room-settings'); +const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); +const assert = require('assert'); + +module.exports = async function e2eEncryptionScenarios(alice, bob) { + console.log(" creating an e2e encrypted room and join through invite:"); + const room = "secrets"; + await createRoom(bob, room); + await changeRoomSettings(bob, {encryption: true}); + // await cancelKeyBackup(bob); + await invite(bob, "@alice:localhost"); + await acceptInvite(alice, room); + // do sas verifcation + bob.log.step(`starts SAS verification with ${alice.username}`); + const bobSasPromise = startSasVerifcation(bob, alice.username); + const aliceSasPromise = acceptSasVerification(alice, bob.username); + // wait in parallel, so they don't deadlock on each other + const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); + assert.deepEqual(bobSas, aliceSas); + bob.log.done(`done (match for ${bobSas.join(", ")})`); + 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}); +} diff --git a/test/end-to-end-tests/src/scenarios/lazy-loading.js b/test/end-to-end-tests/src/scenarios/lazy-loading.js new file mode 100644 index 0000000000..f924f78cf1 --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/lazy-loading.js @@ -0,0 +1,123 @@ +/* +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 {delay} = require('../util'); +const join = require('../usecases/join'); +const sendMessage = require('../usecases/send-message'); +const { + checkTimelineContains, + scrollToTimelineTop +} = require('../usecases/timeline'); +const {createRoom} = require('../usecases/create-room'); +const {getMembersInMemberlist} = require('../usecases/memberlist'); +const changeRoomSettings = require('../usecases/room-settings'); +const {enableLazyLoading} = require('../usecases/settings'); +const assert = require('assert'); + +module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { + console.log(" creating a room for lazy loading member scenarios:"); + const charly1to5 = charlies.slice("charly-1..5", 0, 5); + const charly6to10 = charlies.slice("charly-6..10", 5); + assert(charly1to5.sessions.length, 5); + assert(charly6to10.sessions.length, 5); + await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5); + await checkPaginatedDisplayNames(alice, charly1to5); + await checkMemberList(alice, charly1to5); + await joinCharliesWhileAliceIsOffline(alice, charly6to10); + await checkMemberList(alice, charly6to10); + await charlies.room(alias).leave(); + await delay(1000); + await checkMemberListLacksCharlies(alice, charlies); + await checkMemberListLacksCharlies(bob, charlies); +} + +const room = "Lazy Loading Test"; +const alias = "#lltest:localhost"; +const charlyMsg1 = "hi bob!"; +const charlyMsg2 = "how's it going??"; + +async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) { + await createRoom(bob, room); + await changeRoomSettings(bob, {directory: true, visibility: "public_no_guests", alias}); + // wait for alias to be set by server after clicking "save" + // so the charlies can join it. + await bob.delay(500); + const charlyMembers = await charlies.join(alias); + await charlyMembers.talk(charlyMsg1); + await charlyMembers.talk(charlyMsg2); + bob.log.step("sends 20 messages").mute(); + for(let i = 20; i >= 1; --i) { + await sendMessage(bob, `I will only say this ${i} time(s)!`); + } + bob.log.unmute().done(); + await join(alice, alias); +} + +async function checkPaginatedDisplayNames(alice, charlies) { + await scrollToTimelineTop(alice); + //alice should see 2 messages from every charly with + //the correct display name + const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => { + return charlies.sessions.reduce((messages, charly) => { + return messages.concat({ + sender: charly.displayName(), + body: msgText, + }); + }, messages); + }, []); + await checkTimelineContains(alice, expectedMessages, charlies.log.username); +} + +async function checkMemberList(alice, charlies) { + alice.log.step(`checks the memberlist contains herself, bob and ${charlies.log.username}`); + const displayNames = (await getMembersInMemberlist(alice)).map((m) => m.displayName); + assert(displayNames.includes("alice")); + assert(displayNames.includes("bob")); + charlies.sessions.forEach((charly) => { + assert(displayNames.includes(charly.displayName()), + `${charly.displayName()} should be in the member list, ` + + `only have ${displayNames}`); + }); + alice.log.done(); +} + +async function checkMemberListLacksCharlies(session, charlies) { + session.log.step(`checks the memberlist doesn't contain ${charlies.log.username}`); + const displayNames = (await getMembersInMemberlist(session)).map((m) => m.displayName); + charlies.sessions.forEach((charly) => { + assert(!displayNames.includes(charly.displayName()), + `${charly.displayName()} should not be in the member list, ` + + `only have ${displayNames}`); + }); + session.log.done(); +} + +async function joinCharliesWhileAliceIsOffline(alice, charly6to10) { + await alice.setOffline(true); + await delay(1000); + const members6to10 = await charly6to10.join(alias); + const member6 = members6to10.rooms[0]; + member6.log.step("sends 20 messages").mute(); + for(let i = 20; i >= 1; --i) { + await member6.talk("where is charly?"); + } + member6.log.unmute().done(); + const catchupPromise = alice.waitForNextSuccessfulSync(); + await alice.setOffline(false); + await catchupPromise; + await delay(2000); +} diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js new file mode 100644 index 0000000000..d3c26c07e4 --- /dev/null +++ b/test/end-to-end-tests/src/session.js @@ -0,0 +1,222 @@ +/* +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 puppeteer = require('puppeteer'); +const Logger = require('./logger'); +const LogBuffer = require('./logbuffer'); +const {delay} = require('./util'); + +const DEFAULT_TIMEOUT = 20000; + +module.exports = class RiotSession { + constructor(browser, page, username, riotserver, hsUrl) { + this.browser = browser; + this.page = page; + this.hsUrl = hsUrl; + this.riotserver = riotserver; + this.username = username; + this.consoleLog = new LogBuffer(page, "console", (msg) => `${msg.text()}\n`); + this.networkLog = new LogBuffer(page, "requestfinished", async (req) => { + const type = req.resourceType(); + const response = await req.response(); + return `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + }, true); + this.log = new Logger(this.username); + } + + static async create(username, puppeteerOptions, riotserver, hsUrl, throttleCpuFactor = 1) { + const browser = await puppeteer.launch(puppeteerOptions); + const page = await browser.newPage(); + await page.setViewport({ + width: 1280, + height: 800 + }); + if (throttleCpuFactor !== 1) { + const client = await page.target().createCDPSession(); + console.log("throttling cpu by a factor of", throttleCpuFactor); + await client.send('Emulation.setCPUThrottlingRate', { rate: throttleCpuFactor }); + } + return new RiotSession(browser, page, username, riotserver, hsUrl); + } + + async tryGetInnertext(selector) { + const field = await this.page.$(selector); + if (field != null) { + const text_handle = await field.getProperty('innerText'); + return await text_handle.jsonValue(); + } + return null; + } + + async getElementProperty(handle, property) { + const propHandle = await handle.getProperty(property); + return await propHandle.jsonValue(); + } + + innerText(field) { + return this.getElementProperty(field, 'innerText'); + } + + getOuterHTML(element_handle) { + return this.getElementProperty(field, 'outerHTML'); + } + + consoleLogs() { + return this.consoleLog.buffer; + } + + networkLogs() { + return this.networkLog.buffer; + } + + logXHRRequests() { + let buffer = ""; + this.page.on('requestfinished', async (req) => { + const type = req.resourceType(); + const response = await req.response(); + //if (type === 'xhr' || type === 'fetch') { + buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + // if (req.method() === "POST") { + // buffer += " Post data: " + req.postData(); + // } + //} + }); + return { + logs() { + return buffer; + } + } + } + + async printElements(label, elements) { + console.log(label, await Promise.all(elements.map(getOuterHTML))); + } + + async replaceInputText(input, text) { + // click 3 times to select all text + await input.click({clickCount: 3}); + // waiting here solves not having selected all the text by the 3x click above, + // presumably because of the Field label animation. + await this.delay(300); + // then remove it with backspace + await input.press('Backspace'); + // and type the new text + await input.type(text); + } + + query(selector, timeout = DEFAULT_TIMEOUT) { + return this.page.waitForSelector(selector, {visible: true, timeout}); + } + + async queryAll(selector) { + const timeout = DEFAULT_TIMEOUT; + await this.query(selector, timeout); + return await this.page.$$(selector); + } + + waitForReload() { + const timeout = DEFAULT_TIMEOUT; + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + this.browser.removeEventListener('domcontentloaded', callback); + reject(new Error(`timeout of ${timeout}ms for waitForReload elapsed`)); + }, timeout); + + const callback = async () => { + clearTimeout(timeoutHandle); + resolve(); + }; + + this.page.once('domcontentloaded', callback); + }); + } + + waitForNewPage() { + const timeout = DEFAULT_TIMEOUT; + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + this.browser.removeListener('targetcreated', callback); + reject(new Error(`timeout of ${timeout}ms for waitForNewPage elapsed`)); + }, timeout); + + const callback = async (target) => { + if (target.type() !== 'page') { + return; + } + this.browser.removeListener('targetcreated', callback); + clearTimeout(timeoutHandle); + const page = await target.page(); + resolve(page); + }; + + this.browser.on('targetcreated', callback); + }); + } + + /** wait for a /sync request started after this call that gets a 200 response */ + async waitForNextSuccessfulSync() { + const syncUrls = []; + function onRequest(request) { + if (request.url().indexOf("/sync") !== -1) { + syncUrls.push(request.url()); + } + } + + this.page.on('request', onRequest); + + await this.page.waitForResponse((response) => { + return syncUrls.includes(response.request().url()) && response.status() === 200; + }); + + this.page.removeListener('request', onRequest); + } + + goto(url) { + return this.page.goto(url); + } + + url(path) { + return this.riotserver + path; + } + + delay(ms) { + return delay(ms); + } + + async setOffline(enabled) { + const description = enabled ? "offline" : "back online"; + this.log.step(`goes ${description}`); + await this.page.setOfflineMode(enabled); + this.log.done(); + } + + close() { + return this.browser.close(); + } + + async poll(callback, interval = 100) { + const timeout = DEFAULT_TIMEOUT; + let waited = 0; + while(waited < timeout) { + await this.delay(interval); + waited += interval; + if (await callback()) { + return true; + } + } + return false; + } +} diff --git a/test/end-to-end-tests/src/usecases/README.md b/test/end-to-end-tests/src/usecases/README.md new file mode 100644 index 0000000000..daa990e15c --- /dev/null +++ b/test/end-to-end-tests/src/usecases/README.md @@ -0,0 +1,2 @@ +use cases contains the detailed DOM interactions to perform a given use case, may also do some assertions. +use cases are often used in multiple scenarios. diff --git a/test/end-to-end-tests/src/usecases/accept-invite.js b/test/end-to-end-tests/src/usecases/accept-invite.js new file mode 100644 index 0000000000..bcecf41ed2 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/accept-invite.js @@ -0,0 +1,38 @@ +/* +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.queryAll('.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() === name; + }).inviteHandle; + + await inviteHandle.click(); + + const acceptInvitationLink = await session.query(".mx_RoomPreviewBar_Invite .mx_AccessibleButton_kind_primary"); + await acceptInvitationLink.click(); + + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js new file mode 100644 index 0000000000..88547610f0 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -0,0 +1,48 @@ +/* +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 openRoomDirectory(session) { + const roomDirectoryButton = await session.query('.mx_LeftPanel_explore .mx_AccessibleButton'); + await roomDirectoryButton.click(); +} + +async function createRoom(session, roomName) { + session.log.step(`creates room "${roomName}"`); + + const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); + const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); + const roomsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes("rooms")); + if (roomsIndex === -1) { + throw new Error("could not find room list section that contains rooms in header"); + } + const roomsHeader = roomListHeaders[roomsIndex]; + const addRoomButton = await roomsHeader.$(".mx_RoomSubList_addRoom"); + await addRoomButton.click(); + + + const roomNameInput = await session.query('.mx_CreateRoomDialog_name input'); + await session.replaceInputText(roomNameInput, roomName); + + const createButton = await session.query('.mx_Dialog_primary'); + await createButton.click(); + + await session.query('.mx_MessageComposer'); + session.log.done(); +} + +module.exports = {openRoomDirectory, createRoom}; diff --git a/test/end-to-end-tests/src/usecases/dialog.js b/test/end-to-end-tests/src/usecases/dialog.js new file mode 100644 index 0000000000..58f135de04 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/dialog.js @@ -0,0 +1,50 @@ +/* +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 assertDialog(session, expectedTitle) { + const titleElement = await session.query(".mx_Dialog .mx_Dialog_title"); + const dialogHeader = await session.innerText(titleElement); + assert(dialogHeader, expectedTitle); +} + +async function acceptDialog(session, expectedTitle) { + const foundDialog = await acceptDialogMaybe(session, expectedTitle); + if (!foundDialog) { + throw new Error("could not find a dialog"); + } +} + +async function acceptDialogMaybe(session, expectedTitle) { + let primaryButton = null; + try { + primaryButton = await session.query(".mx_Dialog .mx_Dialog_primary"); + } catch(err) { + return false; + } + if (expectedTitle) { + await assertDialog(session, expectedTitle); + } + await primaryButton.click(); + return true; +} + +module.exports = { + assertDialog, + acceptDialog, + acceptDialogMaybe, +}; diff --git a/test/end-to-end-tests/src/usecases/invite.js b/test/end-to-end-tests/src/usecases/invite.js new file mode 100644 index 0000000000..7b3f8a2b48 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/invite.js @@ -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(1000); + const inviteButton = await session.query(".mx_MemberList_invite"); + await inviteButton.click(); + const inviteTextArea = await session.query(".mx_AddressPickerDialog textarea"); + await inviteTextArea.type(userId); + await inviteTextArea.press("Enter"); + const confirmButton = await session.query(".mx_Dialog_primary"); + await confirmButton.click(); + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/join.js b/test/end-to-end-tests/src/usecases/join.js new file mode 100644 index 0000000000..3c14a76143 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/join.js @@ -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'); +const {openRoomDirectory} = require('./create-room'); + +module.exports = async function join(session, roomName) { + session.log.step(`joins room "${roomName}"`); + await openRoomDirectory(session); + const roomInput = await session.query('.mx_DirectorySearchBox input'); + await session.replaceInputText(roomInput, roomName); + + const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton'); + await joinFirstLink.click(); + await session.query('.mx_MessageComposer'); + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/memberlist.js b/test/end-to-end-tests/src/usecases/memberlist.js new file mode 100644 index 0000000000..5858e82bb8 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/memberlist.js @@ -0,0 +1,70 @@ +/* +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 openMemberInfo(session, name) { + const membersAndNames = await getMembersInMemberlist(session); + const matchingLabel = membersAndNames.filter((m) => { + return m.displayName === name; + }).map((m) => m.label)[0]; + await matchingLabel.click(); +}; + +module.exports.openMemberInfo = openMemberInfo; + +module.exports.verifyDeviceForUser = async function(session, name, expectedDevice) { + session.log.step(`verifies e2e device for ${name}`); + const membersAndNames = await getMembersInMemberlist(session); + const matchingLabel = membersAndNames.filter((m) => { + return m.displayName === name; + }).map((m) => m.label)[0]; + await matchingLabel.click(); + // click verify in member info + const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify"); + await firstVerifyButton.click(); + // expect "Verify device" dialog and click "Begin Verification" + const dialogHeader = await session.innerText(await session.query(".mx_Dialog .mx_Dialog_title")); + assert(dialogHeader, "Verify device"); + const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary") + await beginVerificationButton.click(); + // get emoji SAS labels + const sasLabelElements = await session.queryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); + console.log("my sas labels", sasLabels); + + + const dialogCodeFields = await session.queryAll(".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(); +} + +async function getMembersInMemberlist(session) { + const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name"); + return Promise.all(memberNameElements.map(async (el) => { + return {label: el, displayName: await session.innerText(el)}; + })); +} + +module.exports.getMembersInMemberlist = getMembersInMemberlist; diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js new file mode 100644 index 0000000000..bf3e60d490 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -0,0 +1,99 @@ +/* +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 {acceptDialog} = require('./dialog'); + +async function setSettingsToggle(session, toggle, enabled) { + const className = await session.getElementProperty(toggle, "className"); + const checked = className.includes("mx_ToggleSwitch_on"); + if (checked !== enabled) { + await toggle.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`); + /// XXX delay is needed here, possibly because the header is being rerendered + /// click doesn't do anything otherwise + await session.delay(1000); + const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); + await settingsButton.click(); + //find tabs + const tabButtons = await session.queryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel"); + const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t))); + const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))]; + + const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const isDirectory = generalSwitches[0]; + + if (typeof settings.directory === "boolean") { + session.log.step(`sets directory listing to ${settings.directory}`); + await setSettingsToggle(session, isDirectory, settings.directory); + } + + if (settings.alias) { + session.log.step(`sets alias to ${settings.alias}`); + const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings input[type=text]"); + await session.replaceInputText(aliasField, settings.alias); + const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings .mx_AccessibleButton"); + await addButton.click(); + session.log.done(); + } + + securityTabButton.click(); + await session.delay(500); + const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const e2eEncryptionToggle = securitySwitches[0]; + + if (typeof settings.encryption === "boolean") { + session.log.step(`sets room e2e encryption to ${settings.encryption}`); + const clicked = await setSettingsToggle(session, e2eEncryptionToggle, settings.encryption); + // if enabling, accept beta warning dialog + if (clicked && settings.encryption) { + await acceptDialog(session, "Enable encryption?"); + } + } + + if (settings.visibility) { + session.log.step(`sets visibility to ${settings.visibility}`); + const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]"); + assert.equal(radios.length, 7); + const inviteOnly = radios[0]; + const publicNoGuests = radios[1]; + const publicWithGuests = radios[2]; + + if (settings.visibility === "invite_only") { + await inviteOnly.click(); + } else if (settings.visibility === "public_no_guests") { + await publicNoGuests.click(); + } else if (settings.visibility === "public_with_guests") { + await publicWithGuests.click(); + } else { + throw new Error(`unrecognized room visibility setting: ${settings.visibility}`); + } + session.log.done(); + } + + const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton"); + await closeButton.click(); + + session.log.endGroup(); +} diff --git a/test/end-to-end-tests/src/usecases/send-message.js b/test/end-to-end-tests/src/usecases/send-message.js new file mode 100644 index 0000000000..d3bd02cae3 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/send-message.js @@ -0,0 +1,34 @@ +/* +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 sendMessage(session, message) { + session.log.step(`writes "${message}" in room`); + // 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.query('.mx_MessageComposer_editor'); + // sometimes the focus that type() does internally doesn't seem to work + // and calling click before seems to fix it 🤷 + await composer.click(); + await composer.type(message); + const text = await session.innerText(composer); + assert.equal(text.trim(), message.trim()); + await composer.press("Enter"); + // wait for the message to appear sent + await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)"); + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/settings.js b/test/end-to-end-tests/src/usecases/settings.js new file mode 100644 index 0000000000..903524e6b8 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/settings.js @@ -0,0 +1,53 @@ +/* +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 openSettings(session, section) { + const menuButton = await session.query(".mx_TopLeftMenuButton_name"); + await menuButton.click(); + const settingsItem = await session.query(".mx_TopLeftMenu_icon_settings"); + await settingsItem.click(); + if (section) { + const sectionButton = await session.query(`.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`); + await sectionButton.click(); + } +} + +module.exports.enableLazyLoading = async function(session) { + session.log.step(`enables lazy loading of members in the lab settings`); + const settingsButton = await session.query('.mx_BottomLeftMenu_settings'); + await settingsButton.click(); + const llCheckbox = await session.query("#feature_lazyloading"); + await llCheckbox.click(); + await session.waitForReload(); + const closeButton = await session.query(".mx_RoomHeader_cancelButton"); + await closeButton.click(); + session.log.done(); +} + +module.exports.getE2EDeviceFromSettings = async function(session) { + session.log.step(`gets e2e device/key from settings`); + await openSettings(session, "security"); + const deviceAndKey = await session.queryAll(".mx_SettingsTab_section .mx_SecurityUserSettingsTab_deviceInfo 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_UserSettingsDialog .mx_Dialog_cancelButton"); + await closeButton.click(); + session.log.done(); + return {id, key}; +} diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js new file mode 100644 index 0000000000..014d2ff786 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -0,0 +1,90 @@ +/* +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 signup(session, username, password, homeserver) { + session.log.step("signs up"); + await session.goto(session.url('/#/register')); + // change the homeserver by clicking the advanced section + if (homeserver) { + const advancedButton = await session.query('.mx_ServerTypeSelector_type_Advanced'); + await advancedButton.click(); + + // depending on what HS is configured as the default, the advanced registration + // goes the HS/IS entry directly (for matrix.org) or takes you to the user/pass entry (not matrix.org). + // To work with both, we look for the "Change" link in the user/pass entry but don't fail when we can't find it + // As this link should be visible immediately, and to not slow down the case where it isn't present, + // pick a lower timeout of 5000ms + try { + const changeHsField = await session.query('.mx_AuthBody_editServerDetails', 5000); + if (changeHsField) { + await changeHsField.click(); + } + } catch (err) {} + + const hsInputField = await session.query('#mx_ServerConfig_hsUrl'); + await session.replaceInputText(hsInputField, homeserver); + const nextButton = await session.query('.mx_Login_submit'); + // accept homeserver + await nextButton.click(); + } + //fill out form + const usernameField = await session.query("#mx_RegistrationForm_username"); + const passwordField = await session.query("#mx_RegistrationForm_password"); + const passwordRepeatField = await session.query("#mx_RegistrationForm_passwordConfirm"); + await session.replaceInputText(usernameField, username); + await session.replaceInputText(passwordField, password); + await session.replaceInputText(passwordRepeatField, password); + //wait 300ms because Registration/ServerConfig have a 250ms + //delay to internally set the homeserver url + //see Registration::render and ServerConfig::props::delayTimeMs + await session.delay(300); + /// focus on the button to make sure error validation + /// has happened before checking the form is good to go + const registerButton = await session.query('.mx_Login_submit'); + await registerButton.focus(); + // Password validation is async, wait for it to complete before submit + await session.query(".mx_Field_valid #mx_RegistrationForm_password"); + //check no errors + const error_text = await session.tryGetInnertext('.mx_Login_error'); + assert.strictEqual(!!error_text, false); + //submit form + //await page.screenshot({path: "beforesubmit.png", fullPage: true}); + await registerButton.click(); + + //confirm dialog saying you cant log back in without e-mail + const continueButton = await session.query('.mx_QuestionDialog button.mx_Dialog_primary'); + await continueButton.click(); + + //find the privacy policy checkbox and check it + const policyCheckbox = await session.query('.mx_InteractiveAuthEntryComponents_termsPolicy input'); + await policyCheckbox.click(); + + //now click the 'Accept' button to agree to the privacy policy + const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); + await acceptButton.click(); + + //wait for registration to finish so the hash gets set + //onhashchange better? + + const foundHomeUrl = await session.poll(async () => { + const url = session.page.url(); + return url === session.url('/#/home'); + }); + assert(foundHomeUrl); + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/timeline.js b/test/end-to-end-tests/src/usecases/timeline.js new file mode 100644 index 0000000000..1770e0df9f --- /dev/null +++ b/test/end-to-end-tests/src/usecases/timeline.js @@ -0,0 +1,143 @@ +/* +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.scrollToTimelineTop = async function(session) { + session.log.step(`scrolls to the top of the timeline`); + await session.page.evaluate(() => { + return Promise.resolve().then(async () => { + let timedOut = false; + let timeoutHandle = null; + // set scrollTop to 0 in a loop and check every 50ms + // if content became available (scrollTop not being 0 anymore), + // assume everything is loaded after 3s + do { + const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel"); + if (timelineScrollView && timelineScrollView.scrollTop !== 0) { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + timeoutHandle = setTimeout(() => timedOut = true, 3000); + timelineScrollView.scrollTop = 0; + } else { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + } while (!timedOut) + }); + }) + session.log.done(); +} + +module.exports.receiveMessage = async function(session, expectedMessage) { + session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`); + // wait for a response to come in that contains the message + // crude, but effective + + async function getLastMessage() { + const lastTile = await getLastEventTile(session); + return getMessageFromEventTile(lastTile); + } + + let lastMessage; + await session.poll(async () => { + try { + lastMessage = await getLastMessage(); + } catch(err) { + return false; + } + // stop polling when found the expected message + return lastMessage && + lastMessage.body === expectedMessage.body && + lastMessage.sender === expectedMessage.sender; + }); + assertMessage(lastMessage, expectedMessage); + session.log.done(); +} + + +module.exports.checkTimelineContains = async function (session, expectedMessages, sendersDescription) { + session.log.step(`checks timeline contains ${expectedMessages.length} ` + + `given messages${sendersDescription ? ` from ${sendersDescription}`:""}`); + const eventTiles = await getAllEventTiles(session); + let timelineMessages = await Promise.all(eventTiles.map((eventTile) => { + return getMessageFromEventTile(eventTile); + })); + //filter out tiles that were not messages + timelineMessages = timelineMessages.filter((m) => !!m); + timelineMessages.reduce((prevSender, m) => { + if (m.continuation) { + m.sender = prevSender; + return prevSender; + } else { + return m.sender; + } + }); + + expectedMessages.forEach((expectedMessage) => { + const foundMessage = timelineMessages.find((message) => { + return message.sender === expectedMessage.sender && + message.body === expectedMessage.body; + }); + try { + assertMessage(foundMessage, expectedMessage); + } catch(err) { + console.log("timelineMessages", timelineMessages); + throw err; + } + }); + + session.log.done(); +} + +function assertMessage(foundMessage, expectedMessage) { + assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`); + assert.equal(foundMessage.body, expectedMessage.body); + assert.equal(foundMessage.sender, expectedMessage.sender); + if (expectedMessage.hasOwnProperty("encrypted")) { + assert.equal(foundMessage.encrypted, expectedMessage.encrypted); + } +} + +function getLastEventTile(session) { + return session.query(".mx_EventTile_last"); +} + +function getAllEventTiles(session) { + return session.queryAll(".mx_RoomView_MessageList .mx_EventTile"); +} + +async function getMessageFromEventTile(eventTile) { + const senderElement = await eventTile.$(".mx_SenderProfile_name"); + const className = await (await eventTile.getProperty("className")).jsonValue(); + const classNames = className.split(" "); + const bodyElement = await eventTile.$(".mx_EventTile_body"); + let sender = null; + if (senderElement) { + sender = await(await senderElement.getProperty("innerText")).jsonValue(); + } + if (!bodyElement) { + return null; + } + const body = await(await bodyElement.getProperty("innerText")).jsonValue(); + + return { + sender, + body, + encrypted: classNames.includes("mx_EventTile_verified"), + continuation: classNames.includes("mx_EventTile_continuation"), + }; +} diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js new file mode 100644 index 0000000000..323765bebf --- /dev/null +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -0,0 +1,68 @@ +/* +Copyright 2019 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 {openMemberInfo} = require("./memberlist"); +const {assertDialog, acceptDialog} = require("./dialog"); + +async function assertVerified(session) { + const dialogSubTitle = await session.innerText(await session.query(".mx_Dialog h2")); + assert(dialogSubTitle, "Verified!"); +} + +async function startVerification(session, name) { + await openMemberInfo(session, name); + // click verify in member info + const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify"); + await firstVerifyButton.click(); +} + +async function getSasCodes(session) { + const sasLabelElements = await session.queryAll(".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label"); + const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e))); + return sasLabels; +} + +module.exports.startSasVerifcation = async function(session, name) { + await startVerification(session, name); + // expect "Verify device" dialog and click "Begin Verification" + await assertDialog(session, "Verify device"); + // click "Begin Verification" + await acceptDialog(session); + const sasCodes = await getSasCodes(session); + // click "Verify" + await acceptDialog(session); + await assertVerified(session); + // click "Got it" when verification is done + await acceptDialog(session); + return sasCodes; +}; + +module.exports.acceptSasVerification = async function(session, name) { + await assertDialog(session, "Incoming Verification Request"); + const opponentLabelElement = await session.query(".mx_IncomingSasDialog_opponentProfile h2"); + const opponentLabel = await session.innerText(opponentLabelElement); + assert(opponentLabel, name); + // click "Continue" button + await acceptDialog(session); + const sasCodes = await getSasCodes(session); + // click "Verify" + await acceptDialog(session); + await assertVerified(session); + // click "Got it" when verification is done + await acceptDialog(session); + return sasCodes; +}; diff --git a/test/end-to-end-tests/src/util.js b/test/end-to-end-tests/src/util.js new file mode 100644 index 0000000000..8080d771be --- /dev/null +++ b/test/end-to-end-tests/src/util.js @@ -0,0 +1,27 @@ +/* +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. +*/ + +module.exports.range = function(start, amount, step = 1) { + const r = []; + for (let i = 0; i < amount; ++i) { + r.push(start + (i * step)); + } + return r; +} + +module.exports.delay = function(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js new file mode 100644 index 0000000000..d19b232236 --- /dev/null +++ b/test/end-to-end-tests/start.js @@ -0,0 +1,113 @@ +/* +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 RiotSession = require('./src/session'); +const scenario = require('./src/scenario'); +const RestSessionCreator = require('./src/rest/creator'); +const fs = require("fs"); + +const program = require('commander'); +program + .option('--no-logs', "don't output logs, document html on error", false) + .option('--riot-url [url]', "riot url to test", "http://localhost:5000") + .option('--windowed', "dont run tests headless", false) + .option('--slow-mo', "type at a human speed", false) + .option('--dev-tools', "open chrome devtools in browser window", false) + .option('--throttle-cpu [factor]', "factor to slow down the cpu with", parseFloat, 1.0) + .option('--no-sandbox', "same as puppeteer arg", false) + .option('--log-directory ', 'a directory to dump html and network logs in when the tests fail') + .parse(process.argv); + +const hsUrl = 'http://localhost:5005'; + +async function runTests() { + let sessions = []; + const options = { + slowMo: program.slowMo ? 20 : undefined, + devtools: program.devTools, + headless: !program.windowed, + args: [], + }; + if (!program.sandbox) { + options.args.push('--no-sandbox', '--disable-setuid-sandbox'); + } + 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)`); + options.executablePath = path; + } + + const restCreator = new RestSessionCreator( + 'synapse/installations/consent/env/bin', + hsUrl, + __dirname + ); + + async function createSession(username) { + const session = await RiotSession.create(username, options, program.riotUrl, hsUrl, program.throttleCpu); + sessions.push(session); + return session; + } + + let failure = false; + try { + await scenario(createSession, restCreator); + } catch(err) { + failure = true; + console.log('failure: ', err); + if (program.logDirectory) { + await writeLogs(sessions, program.logDirectory); + } + } + + // 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) { + process.exit(-1); + } else { + console.log('all tests finished successfully'); + } +} + +async function writeLogs(sessions, dir) { + let logs = ""; + for(let i = 0; i < sessions.length; ++i) { + const session = sessions[i]; + const userLogDir = `${dir}/${session.username}`; + fs.mkdirSync(userLogDir); + const consoleLogName = `${userLogDir}/console.log`; + const networkLogName = `${userLogDir}/network.log`; + const appHtmlName = `${userLogDir}/app.html`; + documentHtml = await session.page.content(); + fs.writeFileSync(appHtmlName, documentHtml); + fs.writeFileSync(networkLogName, session.networkLogs()); + fs.writeFileSync(consoleLogName, session.consoleLogs()); + await session.page.screenshot({path: `${userLogDir}/screenshot.png`}); + } + return logs; +} + +runTests().catch(function(err) { + console.log(err); + process.exit(-1); +}); diff --git a/test/end-to-end-tests/synapse/.gitignore b/test/end-to-end-tests/synapse/.gitignore new file mode 100644 index 0000000000..aed68e9f30 --- /dev/null +++ b/test/end-to-end-tests/synapse/.gitignore @@ -0,0 +1,2 @@ +installations +synapse.zip \ No newline at end of file diff --git a/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml new file mode 100644 index 0000000000..e07cf585d8 --- /dev/null +++ b/test/end-to-end-tests/synapse/config-templates/consent/homeserver.yaml @@ -0,0 +1,1098 @@ +# vim:ft=yaml + +## Server ## + +# The domain name of the server, with optional explicit port. +# This is used by remote servers to connect to this server, +# e.g. matrix.org, localhost:8080, etc. +# This is also the last part of your UserID. +# +server_name: "localhost" + +# When running as a daemon, the file to store the pid in +# +pid_file: {{SYNAPSE_ROOT}}homeserver.pid + +# CPU affinity mask. Setting this restricts the CPUs on which the +# process will be scheduled. It is represented as a bitmask, with the +# lowest order bit corresponding to the first logical CPU and the +# highest order bit corresponding to the last logical CPU. Not all CPUs +# may exist on a given system but a mask may specify more CPUs than are +# present. +# +# For example: +# 0x00000001 is processor #0, +# 0x00000003 is processors #0 and #1, +# 0xFFFFFFFF is all processors (#0 through #31). +# +# Pinning a Python process to a single CPU is desirable, because Python +# is inherently single-threaded due to the GIL, and can suffer a +# 30-40% slowdown due to cache blow-out and thread context switching +# if the scheduler happens to schedule the underlying threads across +# different cores. See +# https://www.mirantis.com/blog/improve-performance-python-programs-restricting-single-cpu/. +# +# This setting requires the affinity package to be installed! +# +#cpu_affinity: 0xFFFFFFFF + +# The path to the web client which will be served at /_matrix/client/ +# if 'webclient' is configured under the 'listeners' configuration. +# +#web_client_location: "/path/to/web/root" + +# The public-facing base URL that clients use to access this HS +# (not including _matrix/...). This is the same URL a user would +# enter into the 'custom HS URL' field on their client. If you +# use synapse with a reverse proxy, this should be the URL to reach +# synapse via the proxy. +# +public_baseurl: http://localhost:{{SYNAPSE_PORT}}/ + +# Set the soft limit on the number of file descriptors synapse can use +# Zero is used to indicate synapse should set the soft limit to the +# hard limit. +# +#soft_file_limit: 0 + +# Set to false to disable presence tracking on this homeserver. +# +#use_presence: false + +# The GC threshold parameters to pass to `gc.set_threshold`, if defined +# +#gc_thresholds: [700, 10, 10] + +# Set the limit on the returned events in the timeline in the get +# and sync operations. The default value is -1, means no upper limit. +# +#filter_timeline_limit: 5000 + +# Whether room invites to users on this server should be blocked +# (except those sent by local server admins). The default is False. +# +#block_non_admin_invites: True + +# Room searching +# +# If disabled, new messages will not be indexed for searching and users +# will receive errors when searching for messages. Defaults to enabled. +# +#enable_search: false + +# Restrict federation to the following whitelist of domains. +# N.B. we recommend also firewalling your federation listener to limit +# inbound federation traffic as early as possible, rather than relying +# purely on this application-layer restriction. If not specified, the +# default is to whitelist everything. +# +#federation_domain_whitelist: +# - lon.example.com +# - nyc.example.com +# - syd.example.com + +# List of ports that Synapse should listen on, their purpose and their +# configuration. +# +# Options for each listener include: +# +# port: the TCP port to bind to +# +# bind_addresses: a list of local addresses to listen on. The default is +# 'all local interfaces'. +# +# type: the type of listener. Normally 'http', but other valid options are: +# 'manhole' (see docs/manhole.md), +# 'metrics' (see docs/metrics-howto.rst), +# 'replication' (see docs/workers.rst). +# +# tls: set to true to enable TLS for this listener. Will use the TLS +# key/cert specified in tls_private_key_path / tls_certificate_path. +# +# x_forwarded: Only valid for an 'http' listener. Set to true to use the +# X-Forwarded-For header as the client IP. Useful when Synapse is +# behind a reverse-proxy. +# +# resources: Only valid for an 'http' listener. A list of resources to host +# on this port. Options for each resource are: +# +# names: a list of names of HTTP resources. See below for a list of +# valid resource names. +# +# compress: set to true to enable HTTP comression for this resource. +# +# additional_resources: Only valid for an 'http' listener. A map of +# additional endpoints which should be loaded via dynamic modules. +# +# Valid resource names are: +# +# client: the client-server API (/_matrix/client). Also implies 'media' and +# 'static'. +# +# consent: user consent forms (/_matrix/consent). See +# docs/consent_tracking.md. +# +# federation: the server-server API (/_matrix/federation). Also implies +# 'media', 'keys', 'openid' +# +# keys: the key discovery API (/_matrix/keys). +# +# media: the media API (/_matrix/media). +# +# metrics: the metrics interface. See docs/metrics-howto.rst. +# +# openid: OpenID authentication. +# +# replication: the HTTP replication API (/_synapse/replication). See +# docs/workers.rst. +# +# static: static resources under synapse/static (/_matrix/static). (Mostly +# useful for 'fallback authentication'.) +# +# webclient: A web client. Requires web_client_location to be set. +# +listeners: + # TLS-enabled listener: for when matrix traffic is sent directly to synapse. + # + # Disabled by default. To enable it, uncomment the following. (Note that you + # will also need to give Synapse a TLS key and certificate: see the TLS section + # below.) + # + #- port: 8448 + # type: http + # tls: true + # resources: + # - names: [client, federation] + + # Unsecure HTTP listener: for when matrix traffic passes through a reverse proxy + # that unwraps TLS. + # + # If you plan to use a reverse proxy, please see + # https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst. + # + - port: {{SYNAPSE_PORT}} + tls: false + bind_addresses: ['127.0.0.1'] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + + # example additonal_resources: + # + #additional_resources: + # "/_matrix/my/custom/endpoint": + # module: my_module.CustomRequestHandler + # config: {} + + # Turn on the twisted ssh manhole service on localhost on the given + # port. + # + #- port: 9000 + # bind_addresses: ['::1', '127.0.0.1'] + # type: manhole + + +## Homeserver blocking ## + +# How to reach the server admin, used in ResourceLimitError +# +#admin_contact: 'mailto:admin@server.com' + +# Global blocking +# +#hs_disabled: False +#hs_disabled_message: 'Human readable reason for why the HS is blocked' +#hs_disabled_limit_type: 'error code(str), to help clients decode reason' + +# Monthly Active User Blocking +# +#limit_usage_by_mau: False +#max_mau_value: 50 +#mau_trial_days: 2 + +# If enabled, the metrics for the number of monthly active users will +# be populated, however no one will be limited. If limit_usage_by_mau +# is true, this is implied to be true. +# +#mau_stats_only: False + +# Sometimes the server admin will want to ensure certain accounts are +# never blocked by mau checking. These accounts are specified here. +# +#mau_limit_reserved_threepids: +# - medium: 'email' +# address: 'reserved_user@example.com' + + +## TLS ## + +# PEM-encoded X509 certificate for TLS. +# This certificate, as of Synapse 1.0, will need to be a valid and verifiable +# certificate, signed by a recognised Certificate Authority. +# +# See 'ACME support' below to enable auto-provisioning this certificate via +# Let's Encrypt. +# +# If supplying your own, be sure to use a `.pem` file that includes the +# full certificate chain including any intermediate certificates (for +# instance, if using certbot, use `fullchain.pem` as your certificate, +# not `cert.pem`). +# +#tls_certificate_path: "{{SYNAPSE_ROOT}}localhost.tls.crt" + +# PEM-encoded private key for TLS +# +#tls_private_key_path: "{{SYNAPSE_ROOT}}localhost.tls.key" + +# ACME support: This will configure Synapse to request a valid TLS certificate +# for your configured `server_name` via Let's Encrypt. +# +# Note that provisioning a certificate in this way requires port 80 to be +# routed to Synapse so that it can complete the http-01 ACME challenge. +# By default, if you enable ACME support, Synapse will attempt to listen on +# port 80 for incoming http-01 challenges - however, this will likely fail +# with 'Permission denied' or a similar error. +# +# There are a couple of potential solutions to this: +# +# * If you already have an Apache, Nginx, or similar listening on port 80, +# you can configure Synapse to use an alternate port, and have your web +# server forward the requests. For example, assuming you set 'port: 8009' +# below, on Apache, you would write: +# +# ProxyPass /.well-known/acme-challenge http://localhost:8009/.well-known/acme-challenge +# +# * Alternatively, you can use something like `authbind` to give Synapse +# permission to listen on port 80. +# +acme: + # ACME support is disabled by default. Uncomment the following line + # (and tls_certificate_path and tls_private_key_path above) to enable it. + # + #enabled: true + + # Endpoint to use to request certificates. If you only want to test, + # use Let's Encrypt's staging url: + # https://acme-staging.api.letsencrypt.org/directory + # + #url: https://acme-v01.api.letsencrypt.org/directory + + # Port number to listen on for the HTTP-01 challenge. Change this if + # you are forwarding connections through Apache/Nginx/etc. + # + #port: 80 + + # Local addresses to listen on for incoming connections. + # Again, you may want to change this if you are forwarding connections + # through Apache/Nginx/etc. + # + #bind_addresses: ['::', '0.0.0.0'] + + # How many days remaining on a certificate before it is renewed. + # + #reprovision_threshold: 30 + + # The domain that the certificate should be for. Normally this + # should be the same as your Matrix domain (i.e., 'server_name'), but, + # by putting a file at 'https:///.well-known/matrix/server', + # you can delegate incoming traffic to another server. If you do that, + # you should give the target of the delegation here. + # + # For example: if your 'server_name' is 'example.com', but + # 'https://example.com/.well-known/matrix/server' delegates to + # 'matrix.example.com', you should put 'matrix.example.com' here. + # + # If not set, defaults to your 'server_name'. + # + #domain: matrix.example.com + +# List of allowed TLS fingerprints for this server to publish along +# with the signing keys for this server. Other matrix servers that +# make HTTPS requests to this server will check that the TLS +# certificates returned by this server match one of the fingerprints. +# +# Synapse automatically adds the fingerprint of its own certificate +# to the list. So if federation traffic is handled directly by synapse +# then no modification to the list is required. +# +# If synapse is run behind a load balancer that handles the TLS then it +# will be necessary to add the fingerprints of the certificates used by +# the loadbalancers to this list if they are different to the one +# synapse is using. +# +# Homeservers are permitted to cache the list of TLS fingerprints +# returned in the key responses up to the "valid_until_ts" returned in +# key. It may be necessary to publish the fingerprints of a new +# certificate and wait until the "valid_until_ts" of the previous key +# responses have passed before deploying it. +# +# You can calculate a fingerprint from a given TLS listener via: +# openssl s_client -connect $host:$port < /dev/null 2> /dev/null | +# openssl x509 -outform DER | openssl sha256 -binary | base64 | tr -d '=' +# or by checking matrix.org/federationtester/api/report?server_name=$host +# +#tls_fingerprints: [{"sha256": ""}] + + + +## Database ## + +database: + # The database engine name + name: "sqlite3" + # Arguments to pass to the engine + args: + # Path to the database + database: ":memory:" + +# Number of events to cache in memory. +# +#event_cache_size: 10K + + +## Logging ## + +# A yaml python logging config file +# +log_config: "{{SYNAPSE_ROOT}}localhost.log.config" + + +## Ratelimiting ## + +# Number of messages a client can send per second +# +rc_messages_per_second: 10000 + +# Number of message a client can send before being throttled +# +rc_message_burst_count: 10000 + +# Ratelimiting settings for registration and login. +# +# Each ratelimiting configuration is made of two parameters: +# - per_second: number of requests a client can send per second. +# - burst_count: number of requests a client can send before being throttled. +# +# Synapse currently uses the following configurations: +# - one for registration that ratelimits registration requests based on the +# client's IP address. +# - one for login that ratelimits login requests based on the client's IP +# address. +# - one for login that ratelimits login requests based on the account the +# client is attempting to log into. +# - one for login that ratelimits login requests based on the account the +# client is attempting to log into, based on the amount of failed login +# attempts for this account. +# +# The defaults are as shown below. +# +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +# The federation window size in milliseconds +# +#federation_rc_window_size: 1000 + +# The number of federation requests from a single server in a window +# before the server will delay processing the request. +# +#federation_rc_sleep_limit: 10 + +# The duration in milliseconds to delay processing events from +# remote servers by if they go over the sleep limit. +# +#federation_rc_sleep_delay: 500 + +# The maximum number of concurrent federation requests allowed +# from a single server +# +#federation_rc_reject_limit: 50 + +# The number of federation requests to concurrently process from a +# single server +# +#federation_rc_concurrent: 3 + +# Target outgoing federation transaction frequency for sending read-receipts, +# per-room. +# +# If we end up trying to send out more read-receipts, they will get buffered up +# into fewer transactions. +# +#federation_rr_transactions_per_room_per_second: 50 + + + +# Directory where uploaded images and attachments are stored. +# +media_store_path: "{{SYNAPSE_ROOT}}media_store" + +# Media storage providers allow media to be stored in different +# locations. +# +#media_storage_providers: +# - module: file_system +# # Whether to write new local files. +# store_local: false +# # Whether to write new remote media +# store_remote: false +# # Whether to block upload requests waiting for write to this +# # provider to complete +# store_synchronous: false +# config: +# directory: /mnt/some/other/directory + +# Directory where in-progress uploads are stored. +# +uploads_path: "{{SYNAPSE_ROOT}}uploads" + +# The largest allowed upload size in bytes +# +#max_upload_size: 10M + +# Maximum number of pixels that will be thumbnailed +# +#max_image_pixels: 32M + +# Whether to generate new thumbnails on the fly to precisely match +# the resolution requested by the client. If true then whenever +# a new resolution is requested by the client the server will +# generate a new thumbnail. If false the server will pick a thumbnail +# from a precalculated list. +# +#dynamic_thumbnails: false + +# List of thumbnails to precalculate when an image is uploaded. +# +#thumbnail_sizes: +# - width: 32 +# height: 32 +# method: crop +# - width: 96 +# height: 96 +# method: crop +# - width: 320 +# height: 240 +# method: scale +# - width: 640 +# height: 480 +# method: scale +# - width: 800 +# height: 600 +# method: scale + +# Is the preview URL API enabled? If enabled, you *must* specify +# an explicit url_preview_ip_range_blacklist of IPs that the spider is +# denied from accessing. +# +#url_preview_enabled: false + +# List of IP address CIDR ranges that the URL preview spider is denied +# from accessing. There are no defaults: you must explicitly +# specify a list for URL previewing to work. You should specify any +# internal services in your network that you do not want synapse to try +# to connect to, otherwise anyone in any Matrix room could cause your +# synapse to issue arbitrary GET requests to your internal services, +# causing serious security issues. +# +#url_preview_ip_range_blacklist: +# - '127.0.0.0/8' +# - '10.0.0.0/8' +# - '172.16.0.0/12' +# - '192.168.0.0/16' +# - '100.64.0.0/10' +# - '169.254.0.0/16' +# - '::1/128' +# - 'fe80::/64' +# - 'fc00::/7' +# +# List of IP address CIDR ranges that the URL preview spider is allowed +# to access even if they are specified in url_preview_ip_range_blacklist. +# This is useful for specifying exceptions to wide-ranging blacklisted +# target IP ranges - e.g. for enabling URL previews for a specific private +# website only visible in your network. +# +#url_preview_ip_range_whitelist: +# - '192.168.1.1' + +# Optional list of URL matches that the URL preview spider is +# denied from accessing. You should use url_preview_ip_range_blacklist +# in preference to this, otherwise someone could define a public DNS +# entry that points to a private IP address and circumvent the blacklist. +# This is more useful if you know there is an entire shape of URL that +# you know that will never want synapse to try to spider. +# +# Each list entry is a dictionary of url component attributes as returned +# by urlparse.urlsplit as applied to the absolute form of the URL. See +# https://docs.python.org/2/library/urlparse.html#urlparse.urlsplit +# The values of the dictionary are treated as an filename match pattern +# applied to that component of URLs, unless they start with a ^ in which +# case they are treated as a regular expression match. If all the +# specified component matches for a given list item succeed, the URL is +# blacklisted. +# +#url_preview_url_blacklist: +# # blacklist any URL with a username in its URI +# - username: '*' +# +# # blacklist all *.google.com URLs +# - netloc: 'google.com' +# - netloc: '*.google.com' +# +# # blacklist all plain HTTP URLs +# - scheme: 'http' +# +# # blacklist http(s)://www.acme.com/foo +# - netloc: 'www.acme.com' +# path: '/foo' +# +# # blacklist any URL with a literal IPv4 address +# - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' + +# The largest allowed URL preview spidering size in bytes +# +#max_spider_size: 10M + + +## Captcha ## +# See docs/CAPTCHA_SETUP for full details of configuring this. + +# This Home Server's ReCAPTCHA public key. +# +#recaptcha_public_key: "YOUR_PUBLIC_KEY" + +# This Home Server's ReCAPTCHA private key. +# +#recaptcha_private_key: "YOUR_PRIVATE_KEY" + +# Enables ReCaptcha checks when registering, preventing signup +# unless a captcha is answered. Requires a valid ReCaptcha +# public/private key. +# +#enable_registration_captcha: false + +# A secret key used to bypass the captcha test entirely. +# +#captcha_bypass_secret: "YOUR_SECRET_HERE" + +# The API endpoint to use for verifying m.login.recaptcha responses. +# +#recaptcha_siteverify_api: "https://www.recaptcha.net/recaptcha/api/siteverify" + + +## TURN ## + +# The public URIs of the TURN server to give to clients +# +#turn_uris: [] + +# The shared secret used to compute passwords for the TURN server +# +#turn_shared_secret: "YOUR_SHARED_SECRET" + +# The Username and password if the TURN server needs them and +# does not use a token +# +#turn_username: "TURNSERVER_USERNAME" +#turn_password: "TURNSERVER_PASSWORD" + +# How long generated TURN credentials last +# +#turn_user_lifetime: 1h + +# Whether guests should be allowed to use the TURN server. +# This defaults to True, otherwise VoIP will be unreliable for guests. +# However, it does introduce a slight security risk as it allows users to +# connect to arbitrary endpoints without having first signed up for a +# valid account (e.g. by passing a CAPTCHA). +# +#turn_allow_guests: True + + +## Registration ## +# +# Registration can be rate-limited using the parameters in the "Ratelimiting" +# section of this file. + +# Enable registration for new users. +# +enable_registration: true + +# The user must provide all of the below types of 3PID when registering. +# +#registrations_require_3pid: +# - email +# - msisdn + +# Explicitly disable asking for MSISDNs from the registration +# flow (overrides registrations_require_3pid if MSISDNs are set as required) +# +#disable_msisdn_registration: true + +# Mandate that users are only allowed to associate certain formats of +# 3PIDs with accounts on this server. +# +#allowed_local_3pids: +# - medium: email +# pattern: '.*@matrix\.org' +# - medium: email +# pattern: '.*@vector\.im' +# - medium: msisdn +# pattern: '\+44' + +# If set, allows registration of standard or admin accounts by anyone who +# has the shared secret, even if registration is otherwise disabled. +# +registration_shared_secret: "{{REGISTRATION_SHARED_SECRET}}" + +# Set the number of bcrypt rounds used to generate password hash. +# Larger numbers increase the work factor needed to generate the hash. +# The default number is 12 (which equates to 2^12 rounds). +# N.B. that increasing this will exponentially increase the time required +# to register or login - e.g. 24 => 2^24 rounds which will take >20 mins. +# +#bcrypt_rounds: 12 + +# Allows users to register as guests without a password/email/etc, and +# participate in rooms hosted on this server which have been made +# accessible to anonymous users. +# +#allow_guest_access: false + +# The identity server which we suggest that clients should use when users log +# in on this server. +# +# (By default, no suggestion is made, so it is left up to the client. +# This setting is ignored unless public_baseurl is also set.) +# +#default_identity_server: https://matrix.org + +# The list of identity servers trusted to verify third party +# identifiers by this server. +# +# Also defines the ID server which will be called when an account is +# deactivated (one will be picked arbitrarily). +# +#trusted_third_party_id_servers: +# - matrix.org +# - vector.im + +# Users who register on this homeserver will automatically be joined +# to these rooms +# +#auto_join_rooms: +# - "#example:example.com" + +# Where auto_join_rooms are specified, setting this flag ensures that the +# the rooms exist by creating them when the first user on the +# homeserver registers. +# Setting to false means that if the rooms are not manually created, +# users cannot be auto-joined since they do not exist. +# +#autocreate_auto_join_rooms: true + + +## Metrics ### + +# Enable collection and rendering of performance metrics +# +#enable_metrics: False + +# Enable sentry integration +# NOTE: While attempts are made to ensure that the logs don't contain +# any sensitive information, this cannot be guaranteed. By enabling +# this option the sentry server may therefore receive sensitive +# information, and it in turn may then diseminate sensitive information +# through insecure notification channels if so configured. +# +#sentry: +# dsn: "..." + +# Whether or not to report anonymized homeserver usage statistics. +report_stats: false + + +## API Configuration ## + +# A list of event types that will be included in the room_invite_state +# +#room_invite_state_types: +# - "m.room.join_rules" +# - "m.room.canonical_alias" +# - "m.room.avatar" +# - "m.room.encryption" +# - "m.room.name" + + +# A list of application service config files to use +# +#app_service_config_files: +# - app_service_1.yaml +# - app_service_2.yaml + +# Uncomment to enable tracking of application service IP addresses. Implicitly +# enables MAU tracking for application service users. +# +#track_appservice_user_ips: True + + +# a secret which is used to sign access tokens. If none is specified, +# the registration_shared_secret is used, if one is given; otherwise, +# a secret key is derived from the signing key. +# +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" + +# Used to enable access token expiration. +# +#expire_access_token: False + +# a secret which is used to calculate HMACs for form values, to stop +# falsification of values. Must be specified for the User Consent +# forms to work. +# +form_secret: "{{FORM_SECRET}}" + +## Signing Keys ## + +# Path to the signing key to sign messages with +# +signing_key_path: "{{SYNAPSE_ROOT}}localhost.signing.key" + +# The keys that the server used to sign messages with but won't use +# to sign new messages. E.g. it has lost its private key +# +#old_signing_keys: +# "ed25519:auto": +# # Base64 encoded public key +# key: "The public part of your old signing key." +# # Millisecond POSIX timestamp when the key expired. +# expired_ts: 123456789123 + +# How long key response published by this server is valid for. +# Used to set the valid_until_ts in /key/v2 APIs. +# Determines how quickly servers will query to check which keys +# are still valid. +# +#key_refresh_interval: 1d + +# The trusted servers to download signing keys from. +# +#perspectives: +# servers: +# "matrix.org": +# verify_keys: +# "ed25519:auto": +# key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw" + + +# Enable SAML2 for registration and login. Uses pysaml2. +# +# `sp_config` is the configuration for the pysaml2 Service Provider. +# See pysaml2 docs for format of config. +# +# Default values will be used for the 'entityid' and 'service' settings, +# so it is not normally necessary to specify them unless you need to +# override them. +# +#saml2_config: +# sp_config: +# # point this to the IdP's metadata. You can use either a local file or +# # (preferably) a URL. +# metadata: +# #local: ["saml2/idp.xml"] +# remote: +# - url: https://our_idp/metadata.xml +# +# # The rest of sp_config is just used to generate our metadata xml, and you +# # may well not need it, depending on your setup. Alternatively you +# # may need a whole lot more detail - see the pysaml2 docs! +# +# description: ["My awesome SP", "en"] +# name: ["Test SP", "en"] +# +# organization: +# name: Example com +# display_name: +# - ["Example co", "en"] +# url: "http://example.com" +# +# contact_person: +# - given_name: Bob +# sur_name: "the Sysadmin" +# email_address": ["admin@example.com"] +# contact_type": technical +# +# # Instead of putting the config inline as above, you can specify a +# # separate pysaml2 configuration file: +# # +# config_path: "{{SYNAPSE_ROOT}}sp_conf.py" + + + +# Enable CAS for registration and login. +# +#cas_config: +# enabled: true +# server_url: "https://cas-server.com" +# service_url: "https://homeserver.domain.com:8448" +# #required_attributes: +# # name: value + + +# The JWT needs to contain a globally unique "sub" (subject) claim. +# +#jwt_config: +# enabled: true +# secret: "a secret" +# algorithm: "HS256" + + +password_config: + # Uncomment to disable password login + # + #enabled: false + + # Uncomment and change to a secret random string for extra security. + # DO NOT CHANGE THIS AFTER INITIAL SETUP! + # + #pepper: "EVEN_MORE_SECRET" + + + +# Enable sending emails for notification events +# Defining a custom URL for Riot is only needed if email notifications +# should contain links to a self-hosted installation of Riot; when set +# the "app_name" setting is ignored. +# +# If your SMTP server requires authentication, the optional smtp_user & +# smtp_pass variables should be used +# +#email: +# enable_notifs: false +# smtp_host: "localhost" +# smtp_port: 25 +# smtp_user: "exampleusername" +# smtp_pass: "examplepassword" +# require_transport_security: False +# notif_from: "Your Friendly %(app)s Home Server " +# app_name: Matrix +# # if template_dir is unset, uses the example templates that are part of +# # the Synapse distribution. +# #template_dir: res/templates +# notif_template_html: notif_mail.html +# notif_template_text: notif_mail.txt +# notif_for_new_users: True +# riot_base_url: "http://localhost/riot" + + +#password_providers: +# - module: "ldap_auth_provider.LdapAuthProvider" +# config: +# enabled: true +# uri: "ldap://ldap.example.com:389" +# start_tls: true +# base: "ou=users,dc=example,dc=com" +# attributes: +# uid: "cn" +# mail: "email" +# name: "givenName" +# #bind_dn: +# #bind_password: +# #filter: "(objectClass=posixAccount)" + + + +# Clients requesting push notifications can either have the body of +# the message sent in the notification poke along with other details +# like the sender, or just the event ID and room ID (`event_id_only`). +# If clients choose the former, this option controls whether the +# notification request includes the content of the event (other details +# like the sender are still included). For `event_id_only` push, it +# has no effect. +# +# For modern android devices the notification content will still appear +# because it is loaded by the app. iPhone, however will send a +# notification saying only that a message arrived and who it came from. +# +#push: +# include_content: true + + +#spam_checker: +# module: "my_custom_project.SuperSpamChecker" +# config: +# example_option: 'things' + + +# Uncomment to allow non-server-admin users to create groups on this server +# +#enable_group_creation: true + +# If enabled, non server admins can only create groups with local parts +# starting with this prefix +# +#group_creation_prefix: "unofficial/" + + + +# User Directory configuration +# +# 'enabled' defines whether users can search the user directory. If +# false then empty responses are returned to all queries. Defaults to +# true. +# +# 'search_all_users' defines whether to search all users visible to your HS +# when searching the user directory, rather than limiting to users visible +# in public rooms. Defaults to false. If you set it True, you'll have to run +# UPDATE user_directory_stream_pos SET stream_id = NULL; +# on your database to tell it to rebuild the user_directory search indexes. +# +#user_directory: +# enabled: true +# search_all_users: false + + +# User Consent configuration +# +# for detailed instructions, see +# https://github.com/matrix-org/synapse/blob/master/docs/consent_tracking.md +# +# Parts of this section are required if enabling the 'consent' resource under +# 'listeners', in particular 'template_dir' and 'version'. +# +# 'template_dir' gives the location of the templates for the HTML forms. +# This directory should contain one subdirectory per language (eg, 'en', 'fr'), +# and each language directory should contain the policy document (named as +# '.html') and a success page (success.html). +# +# 'version' specifies the 'current' version of the policy document. It defines +# the version to be served by the consent resource if there is no 'v' +# parameter. +# +# 'server_notice_content', if enabled, will send a user a "Server Notice" +# asking them to consent to the privacy policy. The 'server_notices' section +# must also be configured for this to work. Notices will *not* be sent to +# guest users unless 'send_server_notice_to_guests' is set to true. +# +# 'block_events_error', if set, will block any attempts to send events +# until the user consents to the privacy policy. The value of the setting is +# used as the text of the error. +# +# 'require_at_registration', if enabled, will add a step to the registration +# process, similar to how captcha works. Users will be required to accept the +# policy before their account is created. +# +# 'policy_name' is the display name of the policy users will see when registering +# for an account. Has no effect unless `require_at_registration` is enabled. +# Defaults to "Privacy Policy". +# +user_consent: + template_dir: res/templates/privacy + version: 1.0 + server_notice_content: + msgtype: m.text + body: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + send_server_notice_to_guests: True + block_events_error: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + require_at_registration: true + +# Server Notices room configuration +# +# Uncomment this section to enable a room which can be used to send notices +# from the server to users. It is a special room which cannot be left; notices +# come from a special "notices" user id. +# +# If you uncomment this section, you *must* define the system_mxid_localpart +# setting, which defines the id of the user which will be used to send the +# notices. +# +# It's also possible to override the room name, the display name of the +# "notices" user, and the avatar for the user. +# +server_notices: + system_mxid_localpart: notices + system_mxid_display_name: "Server Notices" + system_mxid_avatar_url: "mxc://localhost:{{SYNAPSE_PORT}}/oumMVlgDnLYFaPVkExemNVVZ" + room_name: "Server Notices" + +# Uncomment to disable searching the public room list. When disabled +# blocks searching local and remote room lists for local and remote +# users by always returning an empty list for all queries. +# +#enable_room_list_search: false + +# The `alias_creation` option controls who's allowed to create aliases +# on this server. +# +# The format of this option is a list of rules that contain globs that +# match against user_id, room_id and the new alias (fully qualified with +# server name). The action in the first rule that matches is taken, +# which can currently either be "allow" or "deny". +# +# Missing user_id/room_id/alias fields default to "*". +# +# If no rules match the request is denied. An empty list means no one +# can create aliases. +# +# Options for the rules include: +# +# user_id: Matches against the creator of the alias +# alias: Matches against the alias being created +# room_id: Matches against the room ID the alias is being pointed at +# action: Whether to "allow" or "deny" the request if the rule matches +# +# The default is: +# +#alias_creation_rules: +# - user_id: "*" +# alias: "*" +# room_id: "*" +# action: allow + +# The `room_list_publication_rules` option controls who can publish and +# which rooms can be published in the public room list. +# +# The format of this option is the same as that for +# `alias_creation_rules`. +# +# If the room has one or more aliases associated with it, only one of +# the aliases needs to match the alias rule. If there are no aliases +# then only rules with `alias: *` match. +# +# If no rules match the request is denied. An empty list means no one +# can publish rooms. +# +# Options for the rules include: +# +# user_id: Matches agaisnt the creator of the alias +# room_id: Matches against the room ID being published +# alias: Matches against any current local or canonical aliases +# associated with the room +# action: Whether to "allow" or "deny" the request if the rule matches +# +# The default is: +# +#room_list_publication_rules: +# - user_id: "*" +# alias: "*" +# room_id: "*" +# action: allow diff --git a/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/1.0.html b/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/1.0.html new file mode 100644 index 0000000000..d4959b4bcb --- /dev/null +++ b/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/1.0.html @@ -0,0 +1,23 @@ + + + + Test Privacy policy + + + {% if has_consented %} +

+ Thank you, you've already accepted the license. +

+ {% else %} +

+ Please accept the license! +

+
+ + + + +
+ {% endif %} + + \ No newline at end of file diff --git a/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/success.html b/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/success.html new file mode 100644 index 0000000000..abe27d87ca --- /dev/null +++ b/test/end-to-end-tests/synapse/config-templates/consent/res/templates/privacy/en/success.html @@ -0,0 +1,9 @@ + + + + Test Privacy policy + + +

Danke schon

+ + \ No newline at end of file diff --git a/test/end-to-end-tests/synapse/install.sh b/test/end-to-end-tests/synapse/install.sh new file mode 100755 index 0000000000..077258072c --- /dev/null +++ b/test/end-to-end-tests/synapse/install.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e + +# config +SYNAPSE_BRANCH=develop +INSTALLATION_NAME=consent +SERVER_DIR=installations/$INSTALLATION_NAME +CONFIG_TEMPLATE=consent +PORT=5005 +# set current directory to script directory +BASE_DIR=$(cd $(dirname $0) && pwd) + +if [ -d $BASE_DIR/$SERVER_DIR ]; then + echo "synapse is already installed" + exit +fi + +cd $BASE_DIR +mkdir -p $SERVER_DIR +cd $SERVER_DIR +virtualenv -p python3 env +source env/bin/activate + +# Having been bitten by pip SSL fail too many times, I don't trust the existing pip +# to be able to --upgrade itself, so grab a new one fresh from source. +curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py +python get-pip.py + +pip install --upgrade setuptools +pip install https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH +# apply configuration +pushd env/bin/ +cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./ + +# Hashes used instead of slashes because we'll get a value back from $(pwd) that'll be +# full of un-escapable slashes. +# Use .bak suffix as using no suffix doesn't work macOS. +sed -i.bak "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml +sed -i.bak "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml +sed -i.bak "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i.bak "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml +sed -i.bak "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml +rm *.bak + +popd +# generate signing keys for signing_key_path +python -m synapse.app.homeserver --generate-keys --config-dir env/bin/ -c env/bin/homeserver.yaml diff --git a/test/end-to-end-tests/synapse/start.sh b/test/end-to-end-tests/synapse/start.sh new file mode 100755 index 0000000000..2ff6ae69d0 --- /dev/null +++ b/test/end-to-end-tests/synapse/start.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) +cd $BASE_DIR +cd installations/consent/env/bin/ +source activate +LOGFILE=$(mktemp) +echo "Synapse log file at $LOGFILE" +./synctl start 2> $LOGFILE +EXIT_CODE=$? +if [ $EXIT_CODE -ne 0 ]; then + cat $LOGFILE +fi +exit $EXIT_CODE diff --git a/test/end-to-end-tests/synapse/stop.sh b/test/end-to-end-tests/synapse/stop.sh new file mode 100755 index 0000000000..08258e5a8c --- /dev/null +++ b/test/end-to-end-tests/synapse/stop.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +BASE_DIR=$(cd $(dirname $0) && pwd) +cd $BASE_DIR +cd installations/consent/env/bin/ +source activate +./synctl stop diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock new file mode 100644 index 0000000000..4379b24946 --- /dev/null +++ b/test/end-to-end-tests/yarn.lock @@ -0,0 +1,759 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "11.12.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.1.tgz#d90123f6c61fdf2f7cddd286ddae891586dd3488" + integrity sha512-sKDlqv6COJrR7ar0+GqqhrXQDzQlMcqMnF2iEU6m9hLo8kxozoAGUazwPyELHlRVmjsbvlnGXjnzyptSXVmceA== + +agent-base@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + +ajv@^6.5.5: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +cheerio@^1.0.0-rc.2: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" + integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs= + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +dom-serializer@0, dom-serializer@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +es6-promise@^4.0.3: + version "4.2.6" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" + integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@^1.6.6: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" + integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= + dependencies: + concat-stream "1.6.2" + debug "2.6.9" + mkdirp "0.5.1" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= + dependencies: + pend "~1.2.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +lodash@^4.15.0, lodash@^4.17.11: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +mime-db@~1.38.0: + version "1.38.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" + integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.22" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" + integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== + dependencies: + mime-db "~1.38.0" + +mime@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" + integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +progress@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-from-env@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= + +psl@^1.1.24, psl@^1.1.28: + version "1.1.31" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" + integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +puppeteer@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.14.0.tgz#828c1926b307200d5fc8289b99df4e13e962d339" + integrity sha512-SayS2wUX/8LF8Yo2Rkpc5nkAu4Jg3qu+OLTDSOZtisVQMB2Z5vjlY2TdPi/5CgZKiZroYIiyUN3sRX63El9iaw== + dependencies: + debug "^4.1.0" + extract-zip "^1.6.6" + https-proxy-agent "^2.2.1" + mime "^2.0.3" + progress "^2.0.1" + proxy-from-env "^1.0.0" + rimraf "^2.6.1" + ws "^6.1.0" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +readable-stream@^2.2.2: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d" + integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +rimraf@^2.6.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +tough-cookie@^2.3.3: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= + dependencies: + fd-slicer "~1.0.1"