diff --git a/.circleci/config.yml b/.circleci/config.yml index b65d79a5..46ffebde 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,113 @@ version: 2.1 +parameters: + browser: + type: enum + enum: ["chrome", "firefox"] + default: "chrome" + bver: + type: enum + enum: ["stable", "beta", "unstable"] + default: "stable" + pr_workflow: + type: boolean + default: true # by default pr workflow will get executed. + tag: + type: string + default: "" # use something like: "2.0.0-beta15" when invoking with parameters. +executors: + machine-executor: + machine: true + generic-executor: + docker: + - image: alpine:3.7 + docker-with-browser: + parameters: + browser: + type: enum + enum: ["chrome", "firefox"] + default: "chrome" + bver: + type: enum + enum: ["stable", "beta", "unstable"] + default: "stable" + docker: + - image: twilio/twilio-video-browsers:<>-<> +commands: + get-code: + steps: + - checkout + - when: + condition: << pipeline.parameters.tag >> + steps: + - run: git checkout << pipeline.parameters.tag >> + set-node-version: + steps: + - run: + name: Set node to version 12 + command: | + sudo npm install -g n + sudo n 12 + get-code-and-dependencies: + steps: + - get-code + - restore_cache: + key: dependency-cache-{{ checksum "package.json" }} + - run: + name: Installing dependencies + command: node -v && npm install --legacy-peer-deps + - save_cache: + key: dependency-cache-{{ checksum "package.json" }} + paths: + - ./node_modules + unit-tests: + steps: + - set-node-version + - get-code-and-dependencies + - run: + name: Running unit tests + command: node -v && npm run build && npm run test:unit + network-tests: + steps: + - get-code-and-dependencies + - run: + name: Running integration tests + command: npm run test:docker + integration-tests: + steps: + - get-code-and-dependencies + - run: + name: Running integration tests + command: npm run build && npm run test:integration jobs: + UnitTests: + executor: docker-with-browser + steps: + - unit-tests + run-network-tests: + parameters: + bver: + type: string + browser: + type: string + executor: + name: machine-executor + environment: + BROWSER: << parameters.browser >> + BVER: << parameters.bver >> + steps: [network-tests] + run-integration-tests: + parameters: + bver: + type: string + browser: + type: string + executor: + name: docker-with-browser + environment: + BROWSER: << parameters.browser >> + BVER: << parameters.bver >> + steps: [integration-tests] trigger-qe-tests: docker: - image: circleci/node:latest @@ -13,15 +120,35 @@ jobs: -d '{"branch":"'v${CIRCLE_TAG:0:1}'","parameters":{"sdk_version":"'$CIRCLE_TAG'","is_rc":true}}' \ $SDKS_QE_CIRCLECI_VOICE_JS_SLAVE_PIPELINE_ENDPOINT workflows: + Pull_Request_Workflow: + when: << pipeline.parameters.pr_workflow >> + jobs: + - UnitTests: + context: dockerhub-pulls + name: Unit Tests + - run-integration-tests: + context: dockerhub-pulls + name: Integration Tests <> <> + matrix: + parameters: + browser: ["chrome", "firefox"] + bver: ["beta", "unstable", "stable"] + - run-network-tests: + context: dockerhub-pulls + name: Network Tests <> <> + matrix: + parameters: + browser: ["chrome", "firefox"] + bver: ["stable"] release-candidate: jobs: - - trigger-qe-tests: - context: sdks-qe - filters: - tags: - only: - - /^\d+\.\d+\.\d+-rc\d+$/ - - /^\d+\.\d+\.\d+-preview\d+-rc\d+$/ - - /^\d+\.\d+\.\d+-beta\d+-rc\d+$/ - branches: - ignore: /.*/ + - trigger-qe-tests: + context: sdks-qe + filters: + tags: + only: + - /^\d+\.\d+\.\d+-rc\d+$/ + - /^\d+\.\d+\.\d+-preview\d+-rc\d+$/ + - /^\d+\.\d+\.\d+-beta\d+-rc\d+$/ + branches: + ignore: /.*/ diff --git a/.circleci/images/docker-compose.yml b/.circleci/images/docker-compose.yml new file mode 100644 index 00000000..892eeb33 --- /dev/null +++ b/.circleci/images/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3' +# Note: This file is used to build run the integration tests. +# for running integration tests +# BROWSER=chrome BVER=stable docker-compose --file=./docker-compose.yml run integrationTests +# to build container with browsers that will be used by integrationTests: +# BROWSER=chrome BVER=stable docker-compose --file=./docker-compose.yml build browserContainer +# to run bash for debugging the container: +# BROWSER=chrome BVER=stable docker-compose --file=./docker-compose.yml run bash +services: + defaults: &defaults + user: root + image: twilio/twilio-video-browsers:${BROWSER}-${BVER} + working_dir: /opt/app + cap_add: + - NET_ADMIN + - NET_RAW + runtimeDefaults: &runtimeDefaults + <<: *defaults + environment: + - ENVIRONMENT + - ACCOUNT_SID + - API_KEY_SID + - API_KEY_SECRET + - APPLICATION_SID + - APPLICATION_SID_STIR + - CALLER_ID + volumes: + - "../../:/opt/app" + - /var/run/docker.sock:/var/run/docker.sock + - /opt/app/node_modules + integrationTests: # runs integration tets. Expects that sources are mounted. + <<: *runtimeDefaults + command: bash -c "npm install -g n && n 12 && npm install --no-optional --no-legacy-peer-deps && npm run build && ls -la /root && ls -la /root/.npm && npm run test:network" + bash: # runs bash shell inside container. helpful for debugging + <<: *runtimeDefaults + command: bash + getVersion: # print browser version installed in the container. + <<: *runtimeDefaults + command: /opt/app/.circleci/images/printbrowserversion.sh diff --git a/.circleci/images/printbrowserversion.sh b/.circleci/images/printbrowserversion.sh new file mode 100644 index 00000000..059dd6f0 --- /dev/null +++ b/.circleci/images/printbrowserversion.sh @@ -0,0 +1,7 @@ +#!/bin/bash +if [ "${BROWSER}" == "firefox" ]; +then + echo $(firefox --version) +else + echo $(google-chrome --version) +fi diff --git a/Dockerfile b/Dockerfile index 0d327c02..e372a19c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,48 +1,13 @@ -FROM node:8.16.0 +ARG IMAGE=twilio/twilio-video-browsers:chrome-stable +FROM $IMAGE -RUN apt-get update \ -&& apt-get install -y \ -libasound2 \ -libpango1.0-0 \ -libxt6 \ -wget \ -bzip2 \ -sudo \ -libdbus-glib-1-2 \ -libgtk-3-0 \ -iptables \ -net-tools \ -&& adduser user1 && adduser user1 sudo && su - user1 +RUN sudo apt-get update +RUN sudo apt-get install -y libasound2 libpango1.0-0 libxt6 wget bzip2 sudo libdbus-glib-1-2 libgtk-3-0 iptables net-tools +RUN sudo groupadd docker +RUN sudo usermod -aG docker user1 +RUN sudo su user1 WORKDIR /app - -ARG BVER='stable' - -RUN echo "Installing Chrome: $BVER" \ -&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ -&& echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list \ -&& apt-get update \ -&& echo "Installing google-chrome-$BVER from apt-get" \ -&& apt-get install -y google-chrome-$BVER \ -&& rm -rf /var/lib/apt/lists/* - -RUN echo "Installing Firefox: $BVER" \ -&& if [ $BVER = "beta" ] \ -;then \ - FIREFOX_DOWNLOAD_URL="https://download.mozilla.org/?product=firefox-beta-latest-ssl&os=linux64&lang=en-US" \ -;elif [ $BVER = "unstable" ] \ -;then \ - FIREFOX_DOWNLOAD_URL="https://download.mozilla.org/?product=firefox-nightly-latest-ssl&os=linux64&lang=en-US" \ -;else \ - FIREFOX_DOWNLOAD_URL="https://download.mozilla.org/?product=firefox-latest-ssl&os=linux64&lang=en-US" \ -;fi \ -&& echo "Firefox Download URL: $FIREFOX_DOWNLOAD_URL" \ -&& mkdir /application \ -&& cd /application \ -&& wget -O - $FIREFOX_DOWNLOAD_URL | tar jx - -ENV FIREFOX_BIN=/application/firefox/firefox - COPY . /app CMD ["bash"] diff --git a/README.md b/README.md index d9438312..82f6d6d3 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ npm run test:integration These tests will run via karma, one at a time, in your system's default Chrome and then Firefox. +Network tests have been split out into their own docker processes, and can be run via + +``` +npm run test:docker +``` + Content Security Policy (CSP) ---------------------------- diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3cc86692..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: '3' -services: - test: - environment: - - ACCOUNT_SID=${ACCOUNT_SID} - - AUTH_TOKEN=${AUTH_TOKEN} - - APPLICATION_SID=${APPLICATION_SID} - - APPLICATION_SID_STIR=${APPLICATION_SID_STIR} - - API_KEY_SECRET=${API_KEY_SECRET} - - API_KEY_SID=${API_KEY_SID} - - BVER=${BVER} - - CALLER_ID=${CALLER_ID} - image: twilio-client:1.0.0 - build: - args: - - BVER=${BVER} - context: . - dockerfile: Dockerfile - cap_add: - - NET_ADMIN - - NET_RAW - working_dir: /app - container_name: twilio-client-integration-test - volumes: - - /var/run/docker.sock:/var/run/docker.sock diff --git a/karma.network.conf.ts b/karma.network.conf.ts new file mode 100644 index 00000000..7ea4627b --- /dev/null +++ b/karma.network.conf.ts @@ -0,0 +1,114 @@ +const fs = require('fs'); +const yaml = require('js-yaml'); +const isDocker = require('is-docker')(); + +if (fs.existsSync(__dirname + '/config.yaml')) { + const creds = yaml.safeLoad(fs.readFileSync(__dirname + '/config.yaml', 'utf8')).prod; + + process.env.ACCOUNT_SID = process.env.ACCOUNT_SID || creds.account_sid; + process.env.API_KEY_SID = process.env.API_KEY_SID || creds.api_key_sid; + process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || creds.api_key_secret; + process.env.APPLICATION_SID = process.env.APPLICATION_SID || creds.app_sid; + process.env.APPLICATION_SID_STIR = process.env.APPLICATION_SID_STIR || creds.app_sid_stir; + process.env.CALLER_ID = process.env.CALLER_ID || creds.caller_id; + process.env.AUTH_TOKEN = process.env.AUTH_TOKEN || creds.auth_token; +} + +module.exports = function(config: any) { + const supportedBrowsers: Record = { + chrome: ['ChromeWebRTC'], + firefox: ['FirefoxWebRTC'], + safari: ['SafariTechPreview'] + }; + + let browsers: string[]; + if (process.env.BROWSER) { + browsers = supportedBrowsers[process.env.BROWSER]; + if (!browsers) { + throw new Error('Unknown browser'); + } + } else if (process.platform === 'darwin') { + browsers = ['ChromeWebRTC', 'FirefoxWebRTC']; + } else { + browsers = ['ChromeWebRTC', 'FirefoxWebRTC']; + } + + const firefoxFlags = []; + const chromeFlags = [ + '--no-sandbox', + '--use-fake-ui-for-media-stream', + '--use-fake-device-for-media-stream', + '--autoplay-policy=no-user-gesture-required', + ]; + + if (isDocker) { + firefoxFlags.push('-headless'); + chromeFlags.push( + '--headless', + '--disable-gpu', + '--remote-debugging-port=9222' + ); + } + + config.set({ + basePath: '', + browsers, + colors: true, + concurrency: 1, + customLaunchers: { + ChromeWebRTC: { + base: 'Chrome', + flags: chromeFlags, + }, + FirefoxWebRTC: { + base: 'Firefox', + flags: firefoxFlags, + prefs: { + 'media.autoplay.default': 0, + 'media.autoplay.enabled': true, + 'media.gstreamer.enabled': true, + 'media.navigator.permission.disabled': true, + 'media.navigator.streams.fake': true, + }, + }, + }, + files: [ + 'lib/twilio.ts', + 'lib/twilio/**/*.+(ts|js)', + 'tests/network/*.ts', + ], + frameworks: ['mocha', 'karma-typescript'], + karmaTypescriptConfig: { + bundlerOptions: { + addNodeGlobals: true, + resolve: { + alias: { + buffer: './node_modules/buffer/index.js', + deprecate: './scripts/noop.js', + } + }, + transforms: [require('karma-typescript-es6-transform')({ + plugins: [ + 'transform-inline-environment-variables', + ] + })], + }, + include: [ + 'lib/**/*', + 'tests/network/**/*.ts', + ], + tsconfig: './tsconfig.json', + }, + logLevel: config.LOG_INFO, + port: 9876, + preprocessors: { + 'lib/**/*.+(ts|js)': 'karma-typescript', + 'tests/network/*.ts': 'karma-typescript', + }, + reporters: ['spec', 'karma-typescript'], + singleRun: true, + browserDisconnectTolerance: 3, + browserDisconnectTimeout : 5000, + browserNoActivityTimeout : 120000, + }); +}; diff --git a/lib/twilio/call.ts b/lib/twilio/call.ts index 79d3e8e9..a664b2b3 100644 --- a/lib/twilio/call.ts +++ b/lib/twilio/call.ts @@ -473,7 +473,6 @@ class Call extends EventEmitter { if (!this._isCancelled) { // tslint:disable no-console - console.info('DISCONNECT!!'); this.emit('disconnect', this); } }; diff --git a/package.json b/package.json index e0047e31..9e6dde1f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "voice", "voip" ], + "engines": { + "node": "0.12" + }, "repository": { "type": "git", "url": "git@github.com:twilio/twilio-voice.js.git" @@ -41,7 +44,7 @@ "start": "node server.js", "status": "git status", "test": "npm-run-all lint build test:unit test:webpack test:es5 test:docker", - "test:docker": "cd tests/docker && ./scripts/run.sh", + "test:docker": "chmod +x ./scripts/circleci-run-tests.sh && ./scripts/circleci-run-tests.sh", "test:es5": "es-check es5 \"./es5/**/*.js\" ./dist/*.js", "test:framework:no-framework": "mocha tests/framework/no-framework.js", "test:framework:react:install": "cd ./tests/framework/react && rimraf ./node_modules package-lock.json && npm install", @@ -49,7 +52,8 @@ "test:framework:react:run": "mocha ./tests/framework/react.js", "test:framework:react": "npm-run-all test:framework:react:*", "test:frameworks": "npm-run-all test:framework:no-framework test:framework:react", - "test:integration": "node ./scripts/karma.js $PWD/karma.conf.ts", + "test:integration": "karma start $PWD/karma.conf.ts", + "test:network": "node ./scripts/karma.js $PWD/karma.network.conf.ts", "test:selenium": "mocha tests/browser/index.js", "test:unit": "nyc mocha -r ts-node/register ./tests/index.ts", "test:webpack": "cd ./tests/webpack && npm install && npm test" diff --git a/scripts/circleci-run-tests.sh b/scripts/circleci-run-tests.sh new file mode 100755 index 00000000..024edbd6 --- /dev/null +++ b/scripts/circleci-run-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# NOTE(mpatwardhan): IMPORTANT - Since CircleCi logs are publicly available, +# DO NOT echo or printenv or in any other way let the sensitive environment variables +# get printed or saved. + +set -ev + +echo "current directory:" +echo $PWD +echo "node version:" +node --version +echo "npm version:" +npm --version +echo "os info:" +uname -a +echo "directory:" +ls -alt +echo "Package.json version:" +cat package.json | grep version +echo "running tests" + + +echo "Running network tests" +# network tets run inside a container with docker socket mapped in the container. + +if [[ -z "${BVER}" ]]; then + export BVER="stable" +fi + +if [[ -z "${BROWSER}" ]]; then + BROWSER="chrome" docker-compose --file=.circleci/images/docker-compose.yml run integrationTests + BROWSER="firefox" docker-compose --file=.circleci/images/docker-compose.yml run integrationTests +else + docker-compose --file=.circleci/images/docker-compose.yml run integrationTests +fi + +echo "Done with Tests!" diff --git a/tests/docker/scripts/run.sh b/tests/docker/scripts/run.sh deleted file mode 100755 index 7c464f41..00000000 --- a/tests/docker/scripts/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Start of containerized test, originating from host machine - -# Bubble up errors -set -e - -# This script can run from anywhere. Let's capture the project directory -DIR=$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) -cd $DIR && cd ../../../ - -echo "Preparing container" -docker-compose build test - -echo "Running the image" -docker-compose run test ./tests/docker/scripts/entry.sh diff --git a/tests/integration/preflight.ts b/tests/integration/preflight.ts index 878e6b9f..4570d200 100644 --- a/tests/integration/preflight.ts +++ b/tests/integration/preflight.ts @@ -6,7 +6,7 @@ import { PreflightTest } from '../../lib/twilio/preflight/preflight'; import Call from '../../lib/twilio/call'; import { TwilioError } from '../../lib/twilio/errors'; -const DURATION_PADDING = 1000; +const DURATION_PADDING = 3000; const EVENT_TIMEOUT = 30000; const MAX_TIMEOUT = 300000; diff --git a/tests/lib/util.js b/tests/lib/util.js index 9aa3f072..e22afe8e 100644 --- a/tests/lib/util.js +++ b/tests/lib/util.js @@ -209,11 +209,13 @@ function product(xs, ys, combine) { * @returns {Promise<*>} */ function runDockerCommand(cmd) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { resolve(this.responseText); + } else if (this.readyState == 4 && this.status != 200) { + reject(this.responseText); } }; xmlhttp.open('GET', `${DOCKER_PROXY_SERVER_URL}/${cmd}`, true); @@ -237,9 +239,22 @@ function waitFor(promiseOrArray, timeoutMS) { return Promise.race([promise, timeoutPromise]).then(() => clearTimeout(timer)); }; +/** + * @returns {Promise} - Resolves to true if caller is running inside docker instance. + * resolves to false if not running inside docker, or if it failed to connect to DockerProxyServer + */ +function isDocker() { + return Promise.resolve() + .then(() => runDockerCommand('isDocker')) + .then(res => res ? JSON.parse(res) : { }) + .then(res => res.isDocker) + .catch(err => console.error('isDocker failed, is server running? ', err)); +} + exports.combinationContext = combinationContext; exports.combinations = combinations; exports.expectEvent = expectEvent; +exports.isDocker = isDocker; exports.isFirefox = isFirefox; exports.pairs = pairs; exports.runDockerCommand = runDockerCommand; diff --git a/tests/integration/reconnection.ts b/tests/network/reconnection.ts similarity index 100% rename from tests/integration/reconnection.ts rename to tests/network/reconnection.ts diff --git a/tests/peerconnection.js b/tests/peerconnection.js index 888884b9..5f2ab130 100644 --- a/tests/peerconnection.js +++ b/tests/peerconnection.js @@ -964,18 +964,21 @@ describe('PeerConnection', () => { context('PeerConnection.prototype._maybeSetIceAggressiveNomination', () => { const METHOD = PeerConnection.prototype._maybeSetIceAggressiveNomination; - const USER_AGENT = root.window.navigator.userAgent; + const navigator = typeof window === 'undefined' + ? root.window.navigator + : window.navigator; + const USER_AGENT = navigator.userAgent; const SDP = 'bar\na=ice-lite\nfoo'; let context; beforeEach(() => { - root.window.navigator.userAgent = 'CriOS'; + navigator.userAgent = 'CriOS'; context = { options: {} }; toTest = METHOD.bind(context); }); afterEach(() => { - root.window.navigator.userAgent = USER_AGENT; + navigator.userAgent = USER_AGENT; }); it('Should call setIceAggressiveNomination if forceAggressiveIceNomination is true', () => { diff --git a/tests/sdp.js b/tests/sdp.js index 6bb5ab43..c805d6c0 100644 --- a/tests/sdp.js +++ b/tests/sdp.js @@ -15,14 +15,17 @@ const { combinationContext } = require('./lib/util'); describe('setIceAggressiveNomination', () => { const SDP_ICE_LITE = 'bar\na=ice-lite\nfoo'; const SDP_FULL_ICE = 'a=group\nfoo\na=ice-options:trickle-ice\n'; - const USER_AGENT = root.window.navigator.userAgent; + const navigator = typeof window === 'undefined' + ? root.window.navigator + : window.navigator; + const USER_AGENT = navigator.userAgent; beforeEach(() => { - root.window.navigator.userAgent = 'CriOS'; + navigator.userAgent = 'CriOS'; }); afterEach(() => { - root.window.navigator.userAgent = USER_AGENT; + navigator.userAgent = USER_AGENT; }); it('should remove ice-lite on chrome', () => { @@ -30,7 +33,7 @@ describe('setIceAggressiveNomination', () => { }); it('should not run on other browsers', () => { - root.window.navigator.userAgent = ''; + navigator.userAgent = ''; assert.equal(setIceAggressiveNomination(SDP_ICE_LITE), SDP_ICE_LITE); });