diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000..b42ad7170f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "trailingComma": "none", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 120 +} diff --git a/karma.conf.js b/karma.conf.js index 92c6bbde5f..7de1582a9b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -103,7 +103,6 @@ module.exports = function(config) { // following files are loaded by RequireJS { pattern: 'browser/static/*.js', included: false }, - { pattern: 'browser/static/*.html', included: false }, { pattern: 'browser/lib/util/base64.js', included: false }, { pattern: 'node_modules/async/lib/async.js', included: false }, @@ -165,7 +164,7 @@ module.exports = function(config) { // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['dots', 'BrowserStack'], + reporters: ['mocha'], reportSlowerThan: 5000, // web server port diff --git a/package-lock.json b/package-lock.json index f58013e4a3..d13103327a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,10343 @@ { "name": "ably", - "version": "1.2.6", - "lockfileVersion": 1, + "version": "1.2.8", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "ably", + "version": "1.2.8", + "license": "Apache-2.0", + "dependencies": { + "@ably/msgpack-js": "^0.3.3", + "request": "^2.87.0", + "ws": "^5.1" + }, + "devDependencies": { + "@ably/vcdiff-decoder": "1.0.4", + "async": "ably-forks/async#requirejs", + "chai": "^4.2.0", + "cors": "~2.7", + "crypto-js": "ably-forks/crypto-js#crypto-lite", + "ejs": "~2.5", + "eslint": "^7.13.0", + "express": "~4.12", + "glob": "~4.4", + "google-closure-compiler": "^20180610.0.1", + "grunt": "~0.4", + "grunt-bump": "^0.3.1", + "grunt-cli": "~1.2.0", + "grunt-closure-tools": "^1.0.0", + "grunt-contrib-concat": "~0.5", + "grunt-shell": "~1.1", + "grunt-webpack": "^4.0.2", + "hexy": "~0.2", + "karma": "ably-forks/karma#ably-js-custom", + "karma-browserstack-launcher": "^1.5.2", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "~0.1", + "karma-cli": "~0.0", + "karma-env-preprocessor": "~0.1", + "karma-firefox-launcher": "~0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-requirejs": "~0.2", + "karma-story-reporter": "~0.3", + "kexec": "ably-forks/node-kexec#update-for-node-12", + "mocha": "^8.1.3", + "null-loader": "^4.0.1", + "requirejs": "~2.1", + "shelljs": "~0.3", + "webpack": "^4.44.2", + "webpack-cli": "^4.2.0" + }, + "engines": { + "node": ">=5.10.x" + } + }, + "node_modules/@ably/msgpack-js": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.3.3.tgz", + "integrity": "sha512-H7oWg97VyA1JhWUP7YN7zwp9W1ozCqMSsqCcXNz4XLmZNdJKT2ntF/6DPgbviFgUpShjQlbPC/iamisTjwLHdQ==", + "dependencies": { + "bops": "~0.0.6" + } + }, + "node_modules/@ably/vcdiff-decoder": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@ably/vcdiff-decoder/-/vcdiff-decoder-1.0.4.tgz", + "integrity": "sha512-D2j7j+keGWOI48ahTjtKmPEahSXtnm2PLVSp1fDCctibyGd/ywRWyJP2TzNtVMwL1IDD9PAaKPPK3+cTo0WP3g==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "node_modules/@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", + "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "dev": true + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "dependencies": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "node_modules/@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.1.0.tgz", + "integrity": "sha512-uNWSdaYHc+f3LdIZNwhdhkjjLDDl3jP2+XBqAq9H8DjrJUvlOKdP8TNruy1yEaDfgpAIgbSAN7pye4FEHg9tYQ==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.1.0.tgz", + "integrity": "sha512-7RfnMXCpJ/NThrhq4gYQYILB18xWyoQcBey81oIyVbmgbc6m5ZHHyFK+DyH7pLHJf0p14MxL4mTsoPAgBSTpIg==", + "dev": true + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", + "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", + "dev": true, + "dependencies": { + "mime-types": "~2.1.6", + "negotiator": "0.5.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "dependencies": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "node_modules/anymatch/node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-back": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", + "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "node_modules/assert/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "dependencies": { + "inherits": "2.0.1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "resolved": "git+https://git@github.com/ably-forks/async.git#76306396b3d3a25316be0170ab6483ccbaaaa097", + "dev": true + }, + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", + "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/batch": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", + "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "dependencies": { + "callsite": "1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "node_modules/bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", + "dev": true + }, + "node_modules/bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bops": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.7.tgz", + "integrity": "sha1-tKClqDmkBkVK8P4FqLkaenZqVOI=", + "dependencies": { + "base64-js": "0.0.2", + "to-utf8": "0.0.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "dependencies": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "dependencies": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserstack": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.3.tgz", + "integrity": "sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg==", + "dev": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + } + }, + "node_modules/browserstack-local": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.4.8.tgz", + "integrity": "sha512-s+mc3gTOJwELdLWi4qFVKtGwMbb5JWsR+JxKlMaJkRJxoZ0gg3WREgPxAN0bm6iU5+S4Bi0sz0oxBRZT8BiNsQ==", + "dev": true, + "dependencies": { + "https-proxy-agent": "^4.0.0", + "is-running": "^2.1.0", + "ps-tree": "=1.2.0", + "temp-fs": "^0.9.9" + } + }, + "node_modules/browserstack-local/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/browserstack-local/node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "node_modules/buffer/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "node_modules/cacache/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cacache/node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "dependencies": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + }, + "optionalDependencies": { + "fsevents": "^1.0.0" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/chokidar/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chokidar/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, + "node_modules/colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.1.tgz", + "integrity": "sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==", + "dev": true, + "dependencies": { + "array-back": "^4.0.1", + "chalk": "^2.4.2", + "table-layout": "^1.0.1", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/connect/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", + "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", + "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "node_modules/copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cors": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.7.2.tgz", + "integrity": "sha1-IThd6//yTCI6EGBbgjEUUpmaHLs=", + "dev": true, + "dependencies": { + "vary": "^1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/crc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz", + "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII=", + "dev": true + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/crypto-js": { + "resolved": "git+https://git@github.com/ably-forks/crypto-js.git#07e09b48fd8f850f71c10c9d9307ca4e6ac622a0", + "dev": true + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "node_modules/cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", + "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-for-each": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deep-for-each/-/deep-for-each-3.0.0.tgz", + "integrity": "sha512-pPN+0f8jlnNP+z90qqOdxGghJU5XM6oBDhvAR+qdQzjCg5pk/7VPPvKK1GqoXEFkHza6ZS+Otzzvmr0g3VUaKw==", + "dev": true, + "dependencies": { + "lodash.isplainobject": "^4.0.6" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", + "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", + "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=", + "dev": true + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=", + "dev": true + }, + "node_modules/ejs": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz", + "integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "dependencies": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.5.tgz", + "integrity": "sha512-j1DWIcktw4hRwrv6nWx++5nFH2X64x16MAG2P0Lmi5Dvdfi3I+Jhc7JKJIdAmDJa+5aZ/imHV7dWRPy2Cqjh3A==", + "dev": true, + "dependencies": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "~1.1.5" + } + }, + "node_modules/engine.io-client": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.5.tgz", + "integrity": "sha512-AYTgHyeVUPitsseqjoedjhYJapNVoSPShbZ+tEUX9/73jgZ/Z3sUlJf9oYgdEBBdVhupUpUqSxH0kBCXlQnmZg==", + "dev": true, + "dependencies": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~1.1.5", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-client/node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "dev": true, + "dependencies": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } + }, + "node_modules/engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "dependencies": { + "mime-types": "~2.1.11", + "negotiator": "0.6.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "dev": true, + "dependencies": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/enhanced-resolve/node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/enhanced-resolve/node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "node_modules/envinfo": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.7.3.tgz", + "integrity": "sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", + "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.13.0.tgz", + "integrity": "sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.6.0.tgz", + "integrity": "sha1-i8ssavElTEgd/IuZfJBu9ORCwgc=", + "dev": true, + "dependencies": { + "crc": "3.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "dependencies": { + "array-slice": "^0.2.3", + "array-unique": "^0.2.1", + "braces": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "dependencies": { + "expand-range": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "dependencies": { + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-braces/node_modules/repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.12.4.tgz", + "integrity": "sha1-j+wlECVbxrLlgQfEgjnA+jB8GqI=", + "dev": true, + "dependencies": { + "accepts": "~1.2.7", + "content-disposition": "0.5.0", + "content-type": "~1.0.1", + "cookie": "0.1.2", + "cookie-signature": "1.0.6", + "debug": "~2.2.0", + "depd": "~1.0.1", + "escape-html": "1.0.1", + "etag": "~1.6.0", + "finalhandler": "0.3.6", + "fresh": "0.2.4", + "merge-descriptors": "1.0.0", + "methods": "~1.1.1", + "on-finished": "~2.2.1", + "parseurl": "~1.3.0", + "path-to-regexp": "0.1.3", + "proxy-addr": "~1.0.8", + "qs": "2.4.2", + "range-parser": "~1.0.2", + "send": "0.12.3", + "serve-static": "~1.9.3", + "type-is": "~1.6.2", + "utils-merge": "1.0.0", + "vary": "~1.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "dependencies": { + "ms": "0.7.1" + } + }, + "node_modules/express/node_modules/ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "node_modules/express/node_modules/qs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz", + "integrity": "sha1-9854jld33wtQENp/fE5zujJHD1o=", + "dev": true + }, + "node_modules/express/node_modules/vary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", + "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.6.tgz", + "integrity": "sha1-2vnEFhsbBuABRmsUEd/baXO+E4s=", + "dev": true, + "dependencies": { + "debug": "~2.2.0", + "escape-html": "1.0.1", + "on-finished": "~2.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "dependencies": { + "ms": "0.7.1" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "dependencies": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/findup-sync/node_modules/glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "dependencies": { + "inherits": "2", + "minimatch": "0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/findup-sync/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true, + "engines": [ + "node", + "rhino" + ] + }, + "node_modules/findup-sync/node_modules/minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/follow-redirects": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", + "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", + "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/fs-write-stream-atomic/node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.4.2.tgz", + "integrity": "sha1-Pvk+KX7glsG5s/+x0hAlx4q2BUg=", + "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^2.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-base/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "dependencies": { + "brace-expansion": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/google-closure-compiler": { + "version": "20180610.0.2", + "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20180610.0.2.tgz", + "integrity": "sha1-Eggy9gzK2ZXo5HxedRIRLVTGro8=", + "dev": true, + "dependencies": { + "chalk": "^1.0.0", + "vinyl": "^2.0.1", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "bin": { + "google-closure-compiler": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/google-closure-compiler/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/google-closure-compiler/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/google-closure-compiler/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/google-closure-compiler/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/google-closure-compiler/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "dependencies": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-bump": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/grunt-bump/-/grunt-bump-0.3.4.tgz", + "integrity": "sha1-dyUehv+L5NvSNK/NXugk5WOaR2c=", + "dev": true, + "dependencies": { + "semver": "^4.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-bump/node_modules/semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/grunt-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "dev": true, + "dependencies": { + "findup-sync": "~0.3.0", + "grunt-known-options": "~1.1.0", + "nopt": "~3.0.6", + "resolve": "~1.1.0" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-cli/node_modules/findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "dependencies": { + "glob": "~5.0.0" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/grunt-cli/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-cli/node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt-closure-tools": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-closure-tools/-/grunt-closure-tools-1.0.0.tgz", + "integrity": "sha1-+pdty8JrZSYgq1pYkYsZnxbpyZ0=", + "dev": true, + "dependencies": { + "grunt": "^1.0.1", + "task-closure-tools": "^0.1.10" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-closure-tools/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "node_modules/grunt-closure-tools/node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/grunt-closure-tools/node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-closure-tools/node_modules/findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "dependencies": { + "glob": "~5.0.0" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/grunt-closure-tools/node_modules/findup-sync/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-closure-tools/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-closure-tools/node_modules/grunt": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.3.0.tgz", + "integrity": "sha512-6ILlMXv11/4cxuhSMfSU+SfvbxrPuqZrAtLN64+tZpQ3DAKfSQPQHRbTjSbdtxfyQhGZPtN0bDZJ/LdCM5WXXA==", + "dev": true, + "dependencies": { + "dateformat": "~3.0.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.2", + "findup-sync": "~0.3.0", + "glob": "~7.1.6", + "grunt-cli": "~1.3.2", + "grunt-known-options": "~1.1.0", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.0", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "mkdirp": "~1.0.4", + "nopt": "~3.0.6", + "rimraf": "~3.0.2" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-closure-tools/node_modules/grunt-legacy-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "dev": true, + "dependencies": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/grunt-closure-tools/node_modules/grunt-legacy-log-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "dev": true, + "dependencies": { + "chalk": "~4.1.0", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-closure-tools/node_modules/grunt-legacy-util": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.0.tgz", + "integrity": "sha512-ZEmYFB44bblwPE2oz3q3ygfF6hseQja9tx8I3UZIwbUik32FMWewA+d1qSFicMFB+8dNXDkh35HcDCWlpRsGlA==", + "dev": true, + "dependencies": { + "async": "~1.5.2", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.20", + "underscore.string": "~3.3.5", + "which": "~1.3.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/grunt-closure-tools/node_modules/grunt/node_modules/grunt-cli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", + "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==", + "dev": true, + "dependencies": { + "grunt-known-options": "~1.1.0", + "interpret": "~1.1.0", + "liftoff": "~2.5.0", + "nopt": "~4.0.1", + "v8flags": "~3.1.1" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-closure-tools/node_modules/grunt/node_modules/grunt-cli/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt-closure-tools/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-closure-tools/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-closure-tools/node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt-closure-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/grunt-closure-tools/node_modules/underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "dev": true, + "dependencies": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-closure-tools/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/grunt-contrib-concat": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-0.5.1.tgz", + "integrity": "sha1-lTxu/f39LBB6uchQd/LUsk0xzUk=", + "dev": true, + "dependencies": { + "chalk": "^0.5.1", + "source-map": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "dependencies": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "dependencies": { + "ansi-regex": "^0.2.0" + }, + "bin": { + "has-ansi": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/source-map": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.3.0.tgz", + "integrity": "sha1-hYb7mloAXltQHiHNGLbyG0V60fk=", + "dev": true, + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "dependencies": { + "ansi-regex": "^0.2.1" + }, + "bin": { + "strip-ansi": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true, + "bin": { + "supports-color": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-known-options": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", + "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "dependencies": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "dependencies": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true, + "engines": [ + "node", + "rhino" + ] + }, + "node_modules/grunt-legacy-log-utils/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-log/node_modules/lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true, + "engines": [ + "node", + "rhino" + ] + }, + "node_modules/grunt-legacy-log/node_modules/underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "dependencies": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-legacy-util/node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "node_modules/grunt-legacy-util/node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true, + "engines": [ + "node", + "rhino" + ] + }, + "node_modules/grunt-legacy-util/node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/grunt-shell": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-1.1.2.tgz", + "integrity": "sha1-Rz5GUwHSnQtW3xb+MQeYznFNCVY=", + "dev": true, + "dependencies": { + "chalk": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-shell/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-shell/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-shell/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-shell/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-shell/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-webpack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/grunt-webpack/-/grunt-webpack-4.0.2.tgz", + "integrity": "sha512-rrqb9SRlY69jEJuCglelB7IvGrI7lRpdfH2GXpFlIOGPRTTtlSxYMU4Fjg8FHaC6ilnMbW5jd55Ff1lR5OibCA==", + "dev": true, + "dependencies": { + "deep-for-each": "^3.0.0", + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/grunt/node_modules/argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "dependencies": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + } + }, + "node_modules/grunt/node_modules/argparse/node_modules/underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "node_modules/grunt/node_modules/esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/grunt/node_modules/glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "dependencies": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "node_modules/grunt/node_modules/js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "dependencies": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/grunt/node_modules/lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true, + "engines": [ + "node", + "rhino" + ] + }, + "node_modules/grunt/node_modules/minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "dependencies": { + "lru-cache": "2", + "sigmund": "~1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/grunt/node_modules/rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/grunt/node_modules/which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true, + "bin": { + "which": "bin/which" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/has-binary/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hexy": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", + "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", + "dev": true, + "bin": { + "hexy": "bin/hexy_cmd.js" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "node_modules/iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", + "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "dependencies": { + "is-primitive": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "dependencies": { + "buffer-alloc": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/karma": { + "resolved": "git+https://git@github.com/ably-forks/karma.git#4e2d5d8b0242e037a319ef33d3c2504280cde6f9", + "dev": true, + "dependencies": { + "batch": "^0.5.3", + "bluebird": "^2.9.27", + "body-parser": "^1.12.4", + "chokidar": "^1.4.1", + "colors": "^1.1.0", + "connect": "^3.3.5", + "core-js": "^2.1.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "expand-braces": "^0.1.1", + "glob": "^7.0.0", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^3.8.0", + "log4js": "^0.6.31", + "mime": "^1.3.4", + "minimatch": "^3.0.0", + "optimist": "^0.6.1", + "rimraf": "^2.3.3", + "socket.io": "^1.4.5", + "source-map": "^0.5.3", + "useragent": "^2.1.6" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": "0.10 || 0.12 || 4 || 5" + } + }, + "node_modules/karma-browserstack-launcher": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.6.0.tgz", + "integrity": "sha512-Y/UWPdHZkHIVH2To4GWHCTzmrsB6H7PBWy6pw+TWz5sr4HW2mcE+Uj6qWgoVNxvQU1Pfn5LQQzI6EQ65p8QbiQ==", + "dev": true, + "dependencies": { + "browserstack": "~1.5.1", + "browserstack-local": "^1.3.7", + "q": "~1.5.0" + } + }, + "node_modules/karma-chai": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", + "integrity": "sha1-vuWtQEAFF4Ea40u5RfdikJEIt5o=", + "dev": true + }, + "node_modules/karma-chrome-launcher": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-0.1.12.tgz", + "integrity": "sha1-CsDiLlc2UPZUExL9ynlcOCTM+WI=", + "dev": true, + "dependencies": { + "which": "^1.0.9" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-cli": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-0.0.4.tgz", + "integrity": "sha1-GECmolRXVLsCEA8JIpzdmWOmz2g=", + "dev": true, + "dependencies": { + "resolve": "~0.5.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": "~0.8 || ~0.10" + } + }, + "node_modules/karma-cli/node_modules/resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.5.1.tgz", + "integrity": "sha1-FeSiIsQja81M+FRUQSwtD7ZSRXY=", + "dev": true + }, + "node_modules/karma-env-preprocessor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/karma-env-preprocessor/-/karma-env-preprocessor-0.1.1.tgz", + "integrity": "sha1-u+jIfVnADtt2BwvTwxtLOdXcfhU=", + "dev": true + }, + "node_modules/karma-firefox-launcher": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-0.1.7.tgz", + "integrity": "sha1-wF3YZTNpHmLzGVJZUJjovTV9OfM=", + "dev": true + }, + "node_modules/karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3" + } + }, + "node_modules/karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=", + "dev": true, + "dependencies": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "peerDependencies": { + "karma": ">=0.13" + } + }, + "node_modules/karma-mocha-reporter/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-mocha-reporter/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-mocha-reporter/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-mocha-reporter/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-requirejs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/karma-requirejs/-/karma-requirejs-0.2.6.tgz", + "integrity": "sha1-GncMZPkBMgo4nGW0lEdGMmNy3vg=", + "dev": true + }, + "node_modules/karma-story-reporter": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/karma-story-reporter/-/karma-story-reporter-0.3.1.tgz", + "integrity": "sha1-t1+qrBRX+qq8RGGjDb08Bsl5+fU=", + "dev": true, + "dependencies": { + "colors": "0.6.0-1" + } + }, + "node_modules/karma-story-reporter/node_modules/colors": { + "version": "0.6.0-1", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.0-1.tgz", + "integrity": "sha1-bbtozri8YPKzE9zFzhWZ8G0Z5no=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/karma/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "node_modules/karma/node_modules/lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "node_modules/kexec": { + "resolved": "git+https://git@github.com/ably-forks/node-kexec.git#f29f54037c7db6ad29e1781463b182e5929215a0", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "nan": "^2.13.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^2.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/liftoff/node_modules/findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/liftoff/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "dependencies": { + "readable-stream": "~1.0.2", + "semver": "~4.3.3" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/log4js/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/log4js/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/log4js/node_modules/semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/log4js/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "dev": true + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz", + "integrity": "sha1-IWnPdTjhsMyH+4jhUC2EdLv3mGQ=", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", + "dev": true, + "bin": { + "mime": "cli.js" + } + }, + "node_modules/mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dependencies": { + "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", + "integrity": "sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.4.3", + "debug": "4.2.0", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "nanoid": "3.1.12", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "7.2.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.2", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/mocha/node_modules/anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mocha/node_modules/binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.1.2" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/mocha/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || >=13.7" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", + "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/null-loader/node_modules/json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/null-loader/node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "dependencies": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.omit/node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz", + "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=", + "dev": true, + "dependencies": { + "ee-first": "1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "dependencies": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "node_modules/optimist/node_modules/minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "dependencies": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "dependencies": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "dependencies": { + "better-assert": "~1.0.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true, + "optional": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", + "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss=", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "dev": true, + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", + "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", + "dev": true, + "dependencies": { + "forwarded": "~0.1.0", + "ipaddr.js": "1.0.5" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", + "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "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" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/readdirp/node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "dependencies": { + "is-equal-shallow": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "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.3", + "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.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/requirejs": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.1.22.tgz", + "integrity": "sha1-3Xj9LTQYDA1ixyS1uK68BmTgNm8=", + "dev": true, + "bin": { + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/send/-/send-0.12.3.tgz", + "integrity": "sha1-zRLcWP3iHk+RkCs5sv2gWnptm9w=", + "dev": true, + "dependencies": { + "debug": "~2.2.0", + "depd": "~1.0.1", + "destroy": "1.0.3", + "escape-html": "1.0.1", + "etag": "~1.6.0", + "fresh": "0.2.4", + "mime": "1.3.4", + "ms": "0.7.1", + "on-finished": "~2.2.1", + "range-parser": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "dependencies": { + "ms": "0.7.1" + } + }, + "node_modules/send/node_modules/ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.9.3.tgz", + "integrity": "sha1-X42gcyOtOF/z3FQfGnkXsuQ261c=", + "dev": true, + "dependencies": { + "escape-html": "1.0.1", + "parseurl": "~1.3.0", + "send": "0.12.3", + "utils-merge": "1.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/socket.io": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.4.tgz", + "integrity": "sha1-L37O3DORvy1cc+KR/iM+bjTU3QA=", + "dev": true, + "dependencies": { + "debug": "2.3.3", + "engine.io": "~1.8.4", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.4", + "socket.io-parser": "2.3.1" + } + }, + "node_modules/socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "dependencies": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/socket.io-client": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.4.tgz", + "integrity": "sha1-7J+CA1btme9tNX8HVtZIcXvdQoE=", + "dev": true, + "dependencies": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "~1.8.4", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + } + }, + "node_modules/socket.io-client/node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "dependencies": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + } + }, + "node_modules/socket.io-parser/node_modules/component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "dependencies": { + "ms": "0.7.1" + } + }, + "node_modules/socket.io-parser/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "node_modules/socket.io/node_modules/debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "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" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "dependencies": { + "figgy-pudding": "^3.5.1" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table-layout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.1.tgz", + "integrity": "sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==", + "dev": true, + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/task-closure-tools": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/task-closure-tools/-/task-closure-tools-0.1.10.tgz", + "integrity": "sha1-2bHs+A7jfi2tIkRbJiAseN0VU3s=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/temp-fs": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", + "integrity": "sha1-gHFzBDeHByDpQxUy/igUNk+IA9c=", + "dev": true, + "dependencies": { + "rimraf": "~2.5.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/temp-fs/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/temp-fs/node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", + "dev": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "dev": true, + "dependencies": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-utf8": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", + "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "node_modules/underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "dependencies": { + "lru-cache": "4.1.x", + "tmp": "0.0.x" + } + }, + "node_modules/useragent/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "node_modules/v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "dev": true, + "dependencies": { + "source-map": "^0.5.1" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" + } + }, + "node_modules/watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "dependencies": { + "chokidar": "^2.1.8" + } + }, + "node_modules/watchpack-chokidar2/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/watchpack-chokidar2/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "node_modules/watchpack-chokidar2/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack/node_modules/anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/watchpack/node_modules/binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "optional": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + } + }, + "node_modules/watchpack/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/watchpack/node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/watchpack/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/watchpack/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack/node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/watchpack/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/webpack": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", + "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.3.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/webpack-cli": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.2.0.tgz", + "integrity": "sha512-EIl3k88vaF4fSxWSgtAQR+VwicfLMTZ9amQtqS4o+TDPW9HGaEpbFBbAZ4A3ZOT5SOnMxNOzROsSTPiE8tBJPA==", + "dev": true, + "dependencies": { + "@webpack-cli/info": "^1.1.0", + "@webpack-cli/serve": "^1.1.0", + "colorette": "^1.2.1", + "command-line-usage": "^6.1.0", + "commander": "^6.2.0", + "enquirer": "^2.3.6", + "execa": "^4.1.0", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "leven": "^3.1.0", + "rechoir": "^0.7.0", + "v8-compile-cache": "^2.2.0", + "webpack-merge": "^4.2.2" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz", + "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-cli/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wordwrapjs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.0.tgz", + "integrity": "sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==", + "dev": true, + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/workerpool": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", + "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "node_modules/xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + } + }, "dependencies": { "@ably/msgpack-js": { "version": "0.3.3", @@ -631,9 +10966,9 @@ "dev": true }, "async": { - "version": "github:ably-forks/async#76306396b3d3a25316be0170ab6483ccbaaaa097", - "from": "github:ably-forks/async#requirejs", - "dev": true + "version": "git+https://git@github.com/ably-forks/async.git#76306396b3d3a25316be0170ab6483ccbaaaa097", + "dev": true, + "from": "async@ably-forks/async#requirejs" }, "async-each": { "version": "1.0.3", @@ -1813,9 +12148,9 @@ } }, "crypto-js": { - "version": "github:ably-forks/crypto-js#07e09b48fd8f850f71c10c9d9307ca4e6ac622a0", - "from": "github:ably-forks/crypto-js#crypto-lite", - "dev": true + "version": "git+https://git@github.com/ably-forks/crypto-js.git#07e09b48fd8f850f71c10c9d9307ca4e6ac622a0", + "dev": true, + "from": "crypto-js@ably-forks/crypto-js#crypto-lite" }, "custom-event": { "version": "1.0.1", @@ -2066,24 +12401,24 @@ "dev": true }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", + "bn.js": "^4.4.0", + "brorand": "^1.0.1", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" }, "dependencies": { "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", "dev": true } } @@ -3139,9 +13474,9 @@ } }, "follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", + "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==", "dev": true }, "for-in": { @@ -4772,9 +15107,9 @@ } }, "karma": { - "version": "github:ably-forks/karma#4e2d5d8b0242e037a319ef33d3c2504280cde6f9", - "from": "github:ably-forks/karma#ably-js-custom", + "version": "git+https://git@github.com/ably-forks/karma.git#4e2d5d8b0242e037a319ef33d3c2504280cde6f9", "dev": true, + "from": "karma@ably-forks/karma#ably-js-custom", "requires": { "batch": "^0.5.3", "bluebird": "^2.9.27", @@ -4910,6 +15245,54 @@ "minimist": "^1.2.3" } }, + "karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha1-FRIAlejtgZGG5HoLAS8810GJVWA=", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "karma-requirejs": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/karma-requirejs/-/karma-requirejs-0.2.6.tgz", @@ -4934,9 +15317,9 @@ } }, "kexec": { - "version": "github:ably-forks/node-kexec#f29f54037c7db6ad29e1781463b182e5929215a0", - "from": "github:ably-forks/node-kexec#update-for-node-12", + "version": "git+https://git@github.com/ably-forks/node-kexec.git#f29f54037c7db6ad29e1781463b182e5929215a0", "dev": true, + "from": "kexec@ably-forks/node-kexec#update-for-node-12", "requires": { "nan": "^2.13.2" } @@ -7293,6 +17676,23 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -7321,23 +17721,6 @@ } } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", diff --git a/package.json b/package.json index fcbac41d97..b891756220 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "karma-env-preprocessor": "~0.1", "karma-firefox-launcher": "~0.1", "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", "karma-requirejs": "~0.2", "karma-story-reporter": "~0.3", "kexec": "ably-forks/node-kexec#update-for-node-12", diff --git a/spec/browser/connection.test.js b/spec/browser/connection.test.js index 5ecb04045b..a26c98be3b 100644 --- a/spec/browser/connection.test.js +++ b/spec/browser/connection.test.js @@ -1,20 +1,19 @@ 'use strict'; -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}, - _exports = {}, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection, - simulateDroppedConnection = helper.simulateDroppedConnection, - transportPreferenceName = 'ably-transport-preference'; - - function supportedBrowser(test) { - if(document.body.ononline === undefined) { +define(['shared_helper', 'chai'], function (helper, chai) { + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; + var simulateDroppedConnection = helper.simulateDroppedConnection; + var transportPreferenceName = 'ably-transport-preference'; + + function supportedBrowser() { + if (document.body.ononline === undefined) { console.log('Online events not supported; skipping connection.test.js'); return false; } - if(!window.WebSocket || !window.localStorage) { + if (!window.WebSocket || !window.localStorage) { console.log('Websockets or local storage not supported; skipping connection.test.js'); return false; } @@ -22,7 +21,7 @@ define(['ably', 'shared_helper'], function(Ably, helper) { // IE doesn't support creating your own events with new try { var testEvent = new Event('foo'); - } catch(e) { + } catch (e) { console.log('On IE; skipping connection.test.js'); return false; } @@ -34,251 +33,375 @@ define(['ably', 'shared_helper'], function(Ably, helper) { window.sessionStorage && window.sessionStorage.removeItem(name); } - exports.setup_realtime = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); - - /* Ensure session is clean for persistance tests */ - eraseSession('ably-connection-recovery'); - }; - - exports.device_going_offline_causes_disconnected_state = function(test) { - var realtime = helper.AblyRealtime(), - connection = realtime.connection, - offlineEvent = new Event('offline', {'bubbles': true}); - - test.expect(3); - monitorConnection(test, realtime); - - connection.once('connected', function() { - var connectedAt = new Date().getTime() - connection.once('disconnected', function() { - var disconnectedAt = new Date().getTime(); - test.ok(disconnectedAt - connectedAt < 1500, 'Offline event caused connection to move to the disconnected state'); - connection.once('connecting', function() { - var reconnectingAt = new Date().getTime(); - test.ok(reconnectingAt - disconnectedAt < 1500, 'Client automatically reattempts connection without waiting for disconnect timeout, even if the state is still offline'); - connection.once('connected', function() { - test.ok(true, 'Successfully reconnected'); - closeAndFinish(test, realtime); - }) + if (supportedBrowser()) { + describe('browser/connection', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); }); - }) - // simulate offline event, expect connection moves to disconnected state and waits to retry connection - document.dispatchEvent(offlineEvent); - }); - }; - - exports.device_going_online_causes_disconnected_connection_to_reconnect_immediately = function(test) { - /* Give up trying to connect fairly quickly */ - var realtime = helper.AblyRealtime({realtimeRequestTimeout: 1000}), - connection = realtime.connection, - onlineEvent = new Event('online', {'bubbles': true}); - - test.expect(3); - monitorConnection(test, realtime); - - // simulate the internet being failed by stubbing out tryATransport to foil - // the initial connection. (No immediate reconnect attempt since it was never - // connected in the first place) - var oldTransport = connection.connectionManager.tryATransport; - connection.connectionManager.tryATransport = function(){}; - - connection.once('disconnected', function() { - var disconnectedAt = new Date(); - test.ok(connection.state == 'disconnected', 'Connection should still be disconnected before we trigger it to connect'); - connection.once('connecting', function() { - test.ok(new Date() - disconnectedAt < 1500, 'Online event should have caused the connection to enter the connecting state without waiting for disconnect timeout'); - connection.once('connected', function() { - test.ok(true, 'Successfully reconnected'); - closeAndFinish(test, realtime); - }) + /* Ensure session is clean for persistance tests */ + eraseSession('ably-connection-recovery'); }); - // restore the 'internet' and simulate an online event - connection.connectionManager.tryATransport= oldTransport; - document.dispatchEvent(onlineEvent); - }); - }; - - exports.device_going_online_causes_suspended_connection_to_reconnect_immediately = function(test) { - /* move to suspended state after 2s of being disconnected */ - var realtime = helper.AblyRealtime({ disconnectedRetryTimeout: 500, realtimeRequestTimeout: 500, connectionStateTtl: 2000 }), - connection = realtime.connection, - onlineEvent = new Event('online', {'bubbles': true}); - // Easiest way to have all transports attempt fail it to stub out tryATransport - connection.connectionManager.tryATransport = function(){}; - - test.expect(2); - connection.on('failed', function () { - test.ok(false, 'connection to server failed'); - closeAndFinish(test, realtime); - }); - - connection.once('suspended', function() { - var suspendedAt = new Date(); - test.ok(connection.state == 'suspended', 'Connection should still be suspended before we trigger it to connect'); - connection.once('connecting', function() { - test.ok(new Date() - suspendedAt < 1500, 'Online event should have caused the connection to enter the connecting state without waiting for suspended timeout'); - closeAndFinish(test, realtime); + it('device_going_offline_causes_disconnected_state', function (done) { + var realtime = helper.AblyRealtime(), + connection = realtime.connection, + offlineEvent = new Event('offline', { bubbles: true }); + + monitorConnection(done, realtime); + + connection.once('connected', function () { + var connectedAt = new Date().getTime(); + connection.once('disconnected', function () { + var disconnectedAt = new Date().getTime(); + try { + expect( + disconnectedAt - connectedAt < 1500, + 'Offline event caused connection to move to the disconnected state' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + connection.once('connecting', function () { + var reconnectingAt = new Date().getTime(); + try { + expect( + reconnectingAt - disconnectedAt < 1500, + 'Client automatically reattempts connection without waiting for disconnect timeout, even if the state is still offline' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + connection.once('connected', function () { + closeAndFinish(done, realtime); + }); + }); + }); + + // simulate offline event, expect connection moves to disconnected state and waits to retry connection + document.dispatchEvent(offlineEvent); + }); }); - // simulate online event - document.dispatchEvent(onlineEvent); - }); - }; - - /* uses internal realtime knowledge of the format of the connection key to - * check if a connection key is the result of a successful recovery of another */ - function sameConnection(keyA, keyB) { - return keyA.split('-')[0] === keyB.split('-')[0]; - } - exports.page_refresh_with_recovery = function(test) { - var realtimeOpts = { recover: function(lastConnectionDetails, cb) { cb(true); } }, - realtime = helper.AblyRealtime(realtimeOpts), - refreshEvent = new Event('beforeunload', {'bubbles': true}); - - test.expect(2); - monitorConnection(test, realtime); - - realtime.connection.once('connected', function() { - var connectionKey = realtime.connection.key; - document.dispatchEvent(refreshEvent); - test.equal(realtime.connection.state, 'connected', 'check connection state initially unaffected by page refresh'); - simulateDroppedConnection(realtime); - - var newRealtime = helper.AblyRealtime(realtimeOpts); - newRealtime.connection.once('connected', function() { - test.ok(sameConnection(connectionKey, newRealtime.connection.key), 'Check new realtime recovered the connection from the cookie'); - closeAndFinish(test, [realtime, newRealtime]); - }); - }); - }; - - exports.page_refresh_persist_with_denied_recovery = function(test) { - var realtimeOpts = { recover: function(lastConnectionDetails, cb) { cb(false); } }; - var realtime = helper.AblyRealtime(realtimeOpts), - refreshEvent = new Event('beforeunload', {'bubbles': true}); - - test.expect(2); - monitorConnection(test, realtime); - - realtime.connection.once('connected', function() { - var connectionKey = realtime.connection.key; - document.dispatchEvent(refreshEvent); - test.equal(realtime.connection.state, 'connected', 'check connection state initially unaffected by page refresh'); - simulateDroppedConnection(realtime); - - var newRealtime = helper.AblyRealtime(realtimeOpts); - newRealtime.connection.once('connected', function() { - test.ok(!sameConnection(connectionKey, newRealtime.connection.key), 'Check new realtime created a new connection'); - closeAndFinish(test, [realtime, newRealtime]); + it('device_going_online_causes_disconnected_connection_to_reconnect_immediately', function (done) { + /* Give up trying to connect fairly quickly */ + var realtime = helper.AblyRealtime({ realtimeRequestTimeout: 1000 }), + connection = realtime.connection, + onlineEvent = new Event('online', { bubbles: true }); + + monitorConnection(done, realtime); + + // simulate the internet being failed by stubbing out tryATransport to foil + // the initial connection. (No immediate reconnect attempt since it was never + // connected in the first place) + var oldTransport = connection.connectionManager.tryATransport; + connection.connectionManager.tryATransport = function () {}; + + connection.once('disconnected', function () { + var disconnectedAt = new Date(); + try { + expect( + connection.state == 'disconnected', + 'Connection should still be disconnected before we trigger it to connect' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + connection.once('connecting', function () { + try { + expect( + new Date() - disconnectedAt < 1500, + 'Online event should have caused the connection to enter the connecting state without waiting for disconnect timeout' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + connection.once('connected', function () { + closeAndFinish(done, realtime); + }); + }); + // restore the 'internet' and simulate an online event + connection.connectionManager.tryATransport = oldTransport; + document.dispatchEvent(onlineEvent); + }); }); - monitorConnection(test, newRealtime); - }); - }; - - exports.page_refresh_with_close_on_unload = function(test) { - var realtime = helper.AblyRealtime({ closeOnUnload: true }), - refreshEvent = new Event('beforeunload', {'bubbles': true}); - - test.expect(1); - monitorConnection(test, realtime); - - realtime.connection.once('connected', function() { - var connectionKey = realtime.connection.key; - document.dispatchEvent(refreshEvent); - var state = realtime.connection.state; - test.ok(state == 'closing' || state == 'closed', 'check page refresh triggered a close'); - test.done(); - }); - }; - - exports.page_refresh_with_manual_recovery = function(test) { - var realtime = helper.AblyRealtime({ closeOnUnload: false }), - refreshEvent = new Event('beforeunload', {'bubbles': true}); - - test.expect(2); - monitorConnection(test, realtime); - realtime.connection.once('connected', function() { - var connectionKey = realtime.connection.key, - recoveryKey = realtime.connection.recoveryKey; - - document.dispatchEvent(refreshEvent); - test.equal(realtime.connection.state, 'connected', 'check connection state initially unaffected by page refresh'); - simulateDroppedConnection(realtime); + it('device_going_online_causes_suspended_connection_to_reconnect_immediately', function (done) { + /* move to suspended state after 2s of being disconnected */ + var realtime = helper.AblyRealtime({ + disconnectedRetryTimeout: 500, + realtimeRequestTimeout: 500, + connectionStateTtl: 2000 + }), + connection = realtime.connection, + onlineEvent = new Event('online', { bubbles: true }); + + // Easiest way to have all transports attempt fail it to stub out tryATransport + connection.connectionManager.tryATransport = function () {}; + + connection.on('failed', function () { + closeAndFinish(done, realtime, new Error('connection to server failed')); + }); - var newRealtime = helper.AblyRealtime({ recover: recoveryKey }); - newRealtime.connection.once('connected', function() { - test.ok(sameConnection(connectionKey, newRealtime.connection.key), 'Check new realtime recovered the old'); - closeAndFinish(test, [realtime, newRealtime]); + connection.once('suspended', function () { + var suspendedAt = new Date(); + try { + expect( + connection.state == 'suspended', + 'Connection should still be suspended before we trigger it to connect' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + connection.once('connecting', function () { + try { + expect( + new Date() - suspendedAt < 1500, + 'Online event should have caused the connection to enter the connecting state without waiting for suspended timeout' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + // simulate online event + document.dispatchEvent(onlineEvent); + }); }); - }); - }; - exports.persist_preferred_transport = function(test) { - test.expect(1); + /* uses internal realtime knowledge of the format of the connection key to + * check if a connection key is the result of a successful recovery of another */ + function sameConnection(keyA, keyB) { + return keyA.split('-')[0] === keyB.split('-')[0]; + } - var realtime = helper.AblyRealtime(); + it('page_refresh_with_recovery', function (done) { + var realtimeOpts = { + recover: function (lastConnectionDetails, cb) { + cb(true); + } + }, + realtime = helper.AblyRealtime(realtimeOpts), + refreshEvent = new Event('beforeunload', { bubbles: true }); + + monitorConnection(done, realtime); + + realtime.connection.once('connected', function () { + var connectionKey = realtime.connection.key; + document.dispatchEvent(refreshEvent); + try { + expect(realtime.connection.state).to.equal( + 'connected', + 'check connection state initially unaffected by page refresh' + ); + simulateDroppedConnection(realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + var newRealtime = helper.AblyRealtime(realtimeOpts); + newRealtime.connection.once('connected', function () { + try { + expect( + sameConnection(connectionKey, newRealtime.connection.key), + 'Check new realtime recovered the connection from the cookie' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, [realtime, newRealtime], err); + return; + } + closeAndFinish(done, [realtime, newRealtime]); + }); + }); + }); - realtime.connection.connectionManager.on(function(transport) { - if(this.event === 'transport.active' && transport.shortName === 'web_socket') { - test.equal(window.localStorage.getItem(transportPreferenceName), JSON.stringify({value: 'web_socket'})); - closeAndFinish(test, realtime); - } - }); - monitorConnection(test, realtime); - }; + it('page_refresh_persist_with_denied_recovery', function (done) { + var realtimeOpts = { + recover: function (lastConnectionDetails, cb) { + cb(false); + } + }; + var realtime = helper.AblyRealtime(realtimeOpts), + refreshEvent = new Event('beforeunload', { bubbles: true }); + + monitorConnection(done, realtime); + + realtime.connection.once('connected', function () { + var connectionKey = realtime.connection.key; + document.dispatchEvent(refreshEvent); + try { + expect(realtime.connection.state).to.equal( + 'connected', + 'check connection state initially unaffected by page refresh' + ); + simulateDroppedConnection(realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + var newRealtime = helper.AblyRealtime(realtimeOpts); + newRealtime.connection.once('connected', function () { + try { + expect( + !sameConnection(connectionKey, newRealtime.connection.key), + 'Check new realtime created a new connection' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, [realtime, newRealtime], err); + return; + } + closeAndFinish(done, [realtime, newRealtime]); + }); + monitorConnection(done, newRealtime); + }); + }); - exports.use_persisted_transport0 = function(test) { - test.expect(1); - var transportPreferenceName = 'ably-transport-preference'; - window.localStorage.setItem(transportPreferenceName, JSON.stringify({value: 'web_socket'})); + it('page_refresh_with_close_on_unload', function (done) { + var realtime = helper.AblyRealtime({ closeOnUnload: true }), + refreshEvent = new Event('beforeunload', { bubbles: true }); + + monitorConnection(done, realtime); + + realtime.connection.once('connected', function () { + try { + var connectionKey = realtime.connection.key; + document.dispatchEvent(refreshEvent); + var state = realtime.connection.state; + expect(state == 'closing' || state == 'closed', 'check page refresh triggered a close').to.be.ok; + } catch (err) { + done(err); + return; + } + done(); + }); + }); - var realtime = helper.AblyRealtime(); + it('page_refresh_with_manual_recovery', function (done) { + var realtime = helper.AblyRealtime({ closeOnUnload: false }), + refreshEvent = new Event('beforeunload', { bubbles: true }); + + monitorConnection(done, realtime); + + realtime.connection.once('connected', function () { + var connectionKey = realtime.connection.key, + recoveryKey = realtime.connection.recoveryKey; + + document.dispatchEvent(refreshEvent); + try { + expect(realtime.connection.state).to.equal( + 'connected', + 'check connection state initially unaffected by page refresh' + ); + simulateDroppedConnection(realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + var newRealtime = helper.AblyRealtime({ recover: recoveryKey }); + newRealtime.connection.once('connected', function () { + try { + expect( + sameConnection(connectionKey, newRealtime.connection.key), + 'Check new realtime recovered the old' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, [realtime, newRealtime], err); + return; + } + closeAndFinish(done, [realtime, newRealtime]); + }); + }); + }); - realtime.connection.connectionManager.on(function(transport) { - if(this.event === 'transport.active') { - test.equal(transport.shortName, 'web_socket'); - closeAndFinish(test, realtime); - } - }); - monitorConnection(test, realtime); - }; + it('persist_preferred_transport', function (done) { + var realtime = helper.AblyRealtime(); + + realtime.connection.connectionManager.on(function (transport) { + if (this.event === 'transport.active' && transport.shortName === 'web_socket') { + try { + expect(window.localStorage.getItem(transportPreferenceName)).to.equal( + JSON.stringify({ value: 'web_socket' }) + ); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } + }); + monitorConnection(done, realtime); + }); - exports.use_persisted_transport1 = function(test) { - test.expect(1); - window.localStorage.setItem(transportPreferenceName, JSON.stringify({value: 'xhr_streaming'})); + it('use_persisted_transport0', function (done) { + var transportPreferenceName = 'ably-transport-preference'; + window.localStorage.setItem(transportPreferenceName, JSON.stringify({ value: 'web_socket' })); + + var realtime = helper.AblyRealtime(); + + realtime.connection.connectionManager.on(function (transport) { + if (this.event === 'transport.active') { + try { + expect(transport.shortName).to.equal('web_socket'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } + }); + monitorConnection(done, realtime); + }); - var realtime = helper.AblyRealtime(); + it('use_persisted_transport1', function (done) { + window.localStorage.setItem(transportPreferenceName, JSON.stringify({ value: 'xhr_streaming' })); + + var realtime = helper.AblyRealtime(); + + realtime.connection.connectionManager.on(function (transport) { + if (this.event === 'transport.active') { + try { + expect(transport.shortName).to.equal('xhr_streaming'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } + }); + monitorConnection(done, realtime); + }); - realtime.connection.connectionManager.on(function(transport) { - if(this.event === 'transport.active') { - test.equal(transport.shortName, 'xhr_streaming'); - closeAndFinish(test, realtime); - } + it('browser_transports', function (done) { + var realtime = helper.AblyRealtime(); + try { + expect(realtime.connection.connectionManager.baseTransport).to.equal('xhr_polling'); + expect(realtime.connection.connectionManager.upgradeTransports).to.deep.equal([ + 'xhr_streaming', + 'web_socket' + ]); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); }); - monitorConnection(test, realtime); - }; - - exports.browser_transports = function(test) { - test.expect(2); - var realtime = helper.AblyRealtime(); - test.equal(realtime.connection.connectionManager.baseTransport, 'xhr_polling'); - test.deepEqual(realtime.connection.connectionManager.upgradeTransports, ['xhr_streaming', 'web_socket']); - closeAndFinish(test, realtime); } +}); - if (supportedBrowser()) { - helper.withMocha('browser/connection', exports); - } -}); \ No newline at end of file diff --git a/spec/browser/simple.test.js b/spec/browser/simple.test.js index 4c4f80d98b..0ccea2fbb2 100644 --- a/spec/browser/simple.test.js +++ b/spec/browser/simple.test.js @@ -1,261 +1,243 @@ -"use strict"; - -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}; - - exports.setupauth = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); +'use strict'; + +define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { + var expect = chai.expect; + + describe('browser/simple', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); }); - }; - function isTransportAvailable(transport) { - return (transport in Ably.Realtime.ConnectionManager.supportedTransports); - } + function isTransportAvailable(transport) { + return transport in Ably.Realtime.ConnectionManager.supportedTransports; + } - function realtimeConnection(transports) { - var options = {}; - if (transports) options.transports = transports; - return helper.AblyRealtime(options); - } + function realtimeConnection(transports) { + var options = {}; + if (transports) options.transports = transports; + return helper.AblyRealtime(options); + } - function failWithin(timeInSeconds, test, ably, description) { - var timeout = setTimeout(function () { - test.ok(false, 'Timed out: Trying to ' + description + ' took longer than ' + timeInSeconds + ' second(s)'); - test.done(); - ably.close(); - }, timeInSeconds * 1000); + function failWithin(timeInSeconds, done, ably, description) { + var timeout = setTimeout(function () { + done(new Error('Timed out: Trying to ' + description + ' took longer than ' + timeInSeconds + ' second(s)')); + ably.close(); + }, timeInSeconds * 1000); - return { - stop: function () { - clearTimeout(timeout); - } - }; - } + return { + stop: function () { + clearTimeout(timeout); + } + }; + } - function connectionWithTransport(test, transport) { - test.expect(1); - try { - var ably = realtimeConnection(transport && [transport]), - connectionTimeout = failWithin(10, test, ably, 'connect'); - ably.connection.on('connected', function () { - connectionTimeout.stop(); - test.ok(true, 'Verify ' + transport + ' connection with key'); - test.done(); - ably.close(); - }); - var exitOnState = function(state) { - ably.connection.on(state, function () { + function connectionWithTransport(done, transport) { + try { + var ably = realtimeConnection(transport && [transport]), + connectionTimeout = failWithin(10, done, ably, 'connect'); + ably.connection.on('connected', function () { connectionTimeout.stop(); - test.ok(false, transport + ' connection to server failed'); - test.done(); + done(); ably.close(); }); - }; - exitOnState('failed'); - exitOnState('suspended'); - } catch (e) { - test.ok(false, 'Init ' + transport + ' connection failed with exception: ' + e); - test.done(); - connectionTimeout.stop(); + var exitOnState = function (state) { + ably.connection.on(state, function () { + connectionTimeout.stop(); + done(new Error(transport + ' connection to server failed')); + ably.close(); + }); + }; + exitOnState('failed'); + exitOnState('suspended'); + } catch (err) { + done(err); + connectionTimeout.stop(); + } + } + + function heartbeatWithTransport(done, transport) { + try { + var ably = realtimeConnection(transport && [transport]), + connectionTimeout = failWithin(10, done, ably, 'connect'), + heartbeatTimeout; + + ably.connection.on('connected', function () { + connectionTimeout.stop(); + heartbeatTimeout = failWithin(25, done, ably, 'wait for heartbeat'); + ably.connection.ping(function (err) { + heartbeatTimeout.stop(); + done(err); + ably.close(); + }); + }); + var exitOnState = function (state) { + ably.connection.on(state, function () { + connectionTimeout.stop(); + done(new Error(transport + ' connection to server failed')); + ably.close(); + }); + }; + exitOnState('failed'); + exitOnState('suspended'); + } catch (err) { + done(err); + connectionTimeout.stop(); + if (heartbeatTimeout) heartbeatTimeout.stop(); + } } - } - function heartbeatWithTransport(test, transport) { - test.expect(1); - try { + function publishWithTransport(done, transport) { + var count = 5; + var sentCount = 0, + receivedCount = 0, + sentCbCount = 0; + var timer; + var checkFinish = function () { + if (receivedCount === count && sentCbCount === count) { + receiveMessagesTimeout.stop(); + done(); + ably.close(); + } + }; var ably = realtimeConnection(transport && [transport]), - connectionTimeout = failWithin(10, test, ably, 'connect'), - heartbeatTimeout; + connectionTimeout = failWithin(5, done, ably, 'connect'), + receiveMessagesTimeout; ably.connection.on('connected', function () { connectionTimeout.stop(); - heartbeatTimeout = failWithin(25, test, ably, 'wait for heartbeat'); - ably.connection.ping(function(err) { - heartbeatTimeout.stop(); - test.ok(!err, 'verify ' + transport + ' heartbeat'); - test.done(); - ably.close(); - }); + receiveMessagesTimeout = failWithin(15, done, ably, 'wait for published messages to be received'); + + timer = setInterval(function () { + channel.publish('event0', 'Hello world at: ' + new Date(), function (err) { + sentCbCount++; + checkFinish(); + }); + if (sentCount === count) clearInterval(timer); + }, 500); }); - var exitOnState = function(state) { - ably.connection.on(state, function () { - connectionTimeout.stop(); - test.ok(false, transport + ' connection to server failed'); - test.done(); - ably.close(); - }); - }; - exitOnState('failed'); - exitOnState('suspended'); - } catch (e) { - test.ok(false, transport + ' connect with key failed with exception: ' + e.stack); - test.done(); - connectionTimeout.stop(); - if (heartbeatTimeout) heartbeatTimeout.stop(); - } - } - function publishWithTransport(test, transport) { - var count = 5; - var sentCount = 0, receivedCount = 0, sentCbCount = 0; - var timer; - var checkFinish = function () { - if ((receivedCount === count) && (sentCbCount === count)) { - receiveMessagesTimeout.stop(); - test.done(); - ably.close(); - } - }; - var ably = realtimeConnection(transport && [transport]), - connectionTimeout = failWithin(5, test, ably, 'connect'), - receiveMessagesTimeout; + var channel = ably.channels.get(transport + 'publish0' + String(Math.random()).substr(1)); + /* subscribe to event */ + channel.subscribe('event0', function () { + receivedCount++; + checkFinish(); + }); + } - ably.connection.on('connected', function () { - connectionTimeout.stop(); - receiveMessagesTimeout = failWithin(15, test, ably, 'wait for published messages to be received'); + it('simpleInitBase0', function (done) { + try { + var timeout, + ably = realtimeConnection(); - timer = setInterval(function () { - console.log('sending: ' + sentCount++); - channel.publish('event0', 'Hello world at: ' + new Date(), function (err) { - console.log(transport + ' publish callback called; err = ' + err); - sentCbCount++; - checkFinish(); + ably.connection.on('connected', function () { + clearTimeout(timeout); + done(); + ably.close(); }); - if (sentCount === count) clearInterval(timer); - }, 500); + var exitOnState = function (state) { + ably.connection.on(state, function () { + done(new Error('Connection to server failed')); + ably.close(); + }); + }; + exitOnState('failed'); + exitOnState('suspended'); + timeout = setTimeout(function () { + done(new Error('Timed out: Trying to connect took longer than expected')); + ably.close(); + }, 10 * 1000); + } catch (err) { + done(err); + } }); - test.expect(count); - var channel = ably.channels.get(transport + 'publish0' + String(Math.random()).substr(1)); - /* subscribe to event */ - channel.subscribe('event0', function () { - test.ok(true, 'Received event0'); - console.log(transport + 'event received'); - receivedCount++; - checkFinish(); - }); - } + var wsTransport = 'web_socket'; + if (isTransportAvailable(wsTransport)) { + it('wsbase0', function (done) { + connectionWithTransport(done, wsTransport); + }); - exports.simpleInitBase0 = function (test) { - test.expect(1); - try { - var timeout, - ably = realtimeConnection(); + /* + * Publish and subscribe, json transport + */ + it('wspublish0', function (done) { + publishWithTransport(done, wsTransport); + }); - ably.connection.on('connected', function () { - clearTimeout(timeout); - test.ok(true, 'Verify init with key'); - test.done(); - ably.close(); + /* + * Check heartbeat + */ + it('wsheartbeat0', function (done) { + heartbeatWithTransport(done, wsTransport); }); - var exitOnState = function(state) { - ably.connection.on(state, function () { - test.ok(false, 'Connection to server failed'); - test.done(); - ably.close(); - }); - }; - exitOnState('failed'); - exitOnState('suspended'); - timeout = setTimeout(function () { - test.ok(false, 'Timed out: Trying to connect took longer than expected'); - test.done(); - ably.close(); - }, 10 * 1000); - } catch (e) { - test.ok(false, 'Init with key failed with exception: ' + e); - test.done(); } - }; - var wsTransport = 'web_socket'; - if(isTransportAvailable(wsTransport)) { - exports.wsbase0 = function (test) { - connectionWithTransport(test, wsTransport); - }; + var xhrTransport = 'xhr'; + if (isTransportAvailable(xhrTransport)) { + it('xhrbase0', function (done) { + connectionWithTransport(done, xhrTransport); + }); - /* - * Publish and subscribe, json transport - */ - exports.wspublish0 = function (test) { - publishWithTransport(test, wsTransport); - }; + /* + * Publish and subscribe, json transport + */ + it('xhrppublish0', function (done) { + publishWithTransport(done, xhrTransport); + }); - /* - * Check heartbeat - */ - exports.wsheartbeat0 = function (test) { - heartbeatWithTransport(test, wsTransport); - }; - } + /* + * Check heartbeat + */ + it('xhrheartbeat0', function (done) { + heartbeatWithTransport(done, xhrTransport); + }); + } - var xhrTransport = 'xhr'; - if(isTransportAvailable(xhrTransport)) { - exports.xhrbase0 = function (test) { - connectionWithTransport(test, xhrTransport); - }; + var jsonpTransport = 'jsonp'; + if (isTransportAvailable(jsonpTransport)) { + it('jsonpbase0', function (done) { + connectionWithTransport(done, jsonpTransport); + }); - /* - * Publish and subscribe, json transport - */ - exports.xhrppublish0 = function (test) { - publishWithTransport(test, xhrTransport); - }; + /* + * Publish and subscribe, json transport + */ + it('jsonppublish0', function (done) { + publishWithTransport(done, jsonpTransport); + }); - /* - * Check heartbeat - */ - exports.xhrheartbeat0 = function (test) { - heartbeatWithTransport(test, xhrTransport); - }; - } + /* + * Check heartbeat + */ + it('jsonpheartbeat0', function (done) { + heartbeatWithTransport(done, jsonpTransport); + }); + } - var jsonpTransport = 'jsonp'; - if(isTransportAvailable(jsonpTransport)) { - exports.jsonpbase0 = function (test) { - connectionWithTransport(test, jsonpTransport); - }; + it('auto_transport_base0', function (done) { + connectionWithTransport(done); + }); /* - * Publish and subscribe, json transport + * Publish and subscribe */ - exports.jsonppublish0 = function (test) { - publishWithTransport(test, jsonpTransport); - }; - + it('auto_transport_publish0', function (done) { + publishWithTransport(done); + }); /* * Check heartbeat */ - exports.jsonpheartbeat0 = function (test) { - heartbeatWithTransport(test, jsonpTransport); - }; - } - - exports.auto_transport_base0 = function (test) { - connectionWithTransport(test); - }; - - /* - * Publish and subscribe - */ - exports.auto_transport_publish0 = function (test) { - publishWithTransport(test); - }; - - /* - * Check heartbeat - */ - exports.auto_transport_heartbeat0 = function (test) { - heartbeatWithTransport(test); - }; + it('auto_transport_heartbeat0', function (done) { + heartbeatWithTransport(done); + }); + }); +}); - helper.withMocha('browser/simple', exports); -}); \ No newline at end of file diff --git a/spec/common/globals/named_dependencies.js b/spec/common/globals/named_dependencies.js index bf6e338576..6dfb9cb602 100644 --- a/spec/common/globals/named_dependencies.js +++ b/spec/common/globals/named_dependencies.js @@ -11,6 +11,7 @@ define(function() { // test modules 'globals': { browser: 'spec/common/globals/environment', node: 'spec/common/globals/environment' }, 'shared_helper': { browser: 'spec/common/modules/shared_helper', node: 'spec/common/modules/shared_helper' }, - 'async': { browser: 'node_modules/async/lib/async' } + 'async': { browser: 'node_modules/async/lib/async' }, + 'chai': { browser: 'node_modules/chai/chai', node: 'node_modules/chai/chai' } }; }); diff --git a/spec/common/modules/shared_helper.js b/spec/common/modules/shared_helper.js index eb48db195a..9610cefa0b 100644 --- a/spec/common/modules/shared_helper.js +++ b/spec/common/modules/shared_helper.js @@ -3,8 +3,8 @@ /* Shared test helper for the Jasmine test suite that simplifies the dependencies by providing common methods in a single dependency */ -define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module', 'spec/common/modules/testapp_manager', 'async', 'node_modules/chai/chai'], - function(testAppModule, clientModule, testAppManager, async, chai) { +define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module', 'spec/common/modules/testapp_manager', 'async'], + function(testAppModule, clientModule, testAppManager, async) { var utils = clientModule.Ably.Realtime.Utils; var supportedTransports = utils.keysArray(clientModule.Ably.Realtime.ConnectionManager.supportedTransports), /* Don't include jsonp in availableTransports if xhr works. Why? Because @@ -34,28 +34,27 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module return result; } - function monitorConnection(test, realtime) { + function monitorConnection(done, realtime) { utils.arrForEach(['failed', 'suspended'], function(state) { realtime.connection.on(state, function () { - test.expect(false, 'Connection monitoring: state changed to ' + state + ', aborting test'); - test.done(); + done(new Error('Connection monitoring: state changed to ' + state + ', aborting test')); realtime.close(); }); }); } - function closeAndFinish(test, realtime) { + function closeAndFinish(done, realtime, err) { if(typeof realtime === 'undefined') { // Likely called in a catch block for an exception // that occured before realtime was initialized - test.done(); + done(err); return; } if(Object.prototype.toString.call(realtime) == '[object Array]') { - closeAndFinishSeveral(test, realtime); + closeAndFinishSeveral(done, realtime, err); return; } - callbackOnClose(realtime, function(){ test.done(); }) + callbackOnClose(realtime, function(){ done(err); }) } function simulateDroppedConnection(realtime) { @@ -78,7 +77,6 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module function callbackOnClose(realtime, callback) { if(!realtime.connection.connectionManager.activeProtocol) { - console.log("No transport established; closing connection and calling done()"); utils.nextTick(function() { realtime.close(); callback(); @@ -86,7 +84,6 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module return; } realtime.connection.connectionManager.activeProtocol.transport.on('disposed', function() { - console.log("Transport disposed; calling done()") callback(); }); /* wait a tick before closing in order to avoid the final close @@ -97,7 +94,7 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module }); } - function closeAndFinishSeveral(test, realtimeArray) { + function closeAndFinishSeveral(done, realtimeArray, e) { async.map(realtimeArray, function(realtime, mapCb){ var parallelItem = function(parallelCb) { callbackOnClose(realtime, function(){ parallelCb(); }) @@ -105,31 +102,39 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module mapCb(null, parallelItem) }, function(err, parallelItems) { async.parallel(parallelItems, function() { - test.done(); + if (err) { + done(err); + return; + } + done(e); }); } ) } /* testFn is assumed to be a function of realtimeOptions that returns a mocha test */ - function testOnAllTransports(exports, name, testFn, excludeUpgrade) { + function testOnAllTransports(name, testFn, excludeUpgrade) { utils.arrForEach(availableTransports, function(transport) { - exports[name + '_with_' + transport + '_binary_transport'] = testFn({transports: [transport], useBinaryProtocol: true}); - exports[name + '_with_' + transport + '_text_transport'] = testFn({transports: [transport], useBinaryProtocol: false}); + it(name + '_with_' + transport + '_binary_transport', testFn({transports: [transport], useBinaryProtocol: true})); + it(name + '_with_' + transport + '_text_transport', testFn({transports: [transport], useBinaryProtocol: false})); }); /* Plus one for no transport specified (ie use upgrade mechanism if * present). (we explicitly specify all transports since node only does * nodecomet+upgrade if comet is explicitly requested * */ if(!excludeUpgrade) { - exports[name + '_with_binary_transport'] = testFn({transports: availableTransports, useBinaryProtocol: true}); - exports[name + '_with_text_transport'] = testFn({transports: availableTransports, useBinaryProtocol: false}); + it(name + '_with_binary_transport', testFn({transports: availableTransports, useBinaryProtocol: true})); + it(name + '_with_text_transport', testFn({transports: availableTransports, useBinaryProtocol: false})); } } - function restTestOnJsonMsgpack(exports, name, testFn) { - exports[name + '_binary'] = function(test) { testFn(test, new clientModule.AblyRest({useBinaryProtocol: true}), name + '_binary'); }; - exports[name + '_text'] = function(test) { testFn(test, new clientModule.AblyRest({useBinaryProtocol: false}), name + '_text'); }; + function restTestOnJsonMsgpack(name, testFn) { + it(name + ' with binary protocol', function (done) { + testFn(done, new clientModule.AblyRest({useBinaryProtocol: true}), name + '_binary'); + }); + it(name + ' with text protocol', function (done) { + testFn(done, new clientModule.AblyRest({useBinaryProtocol: false}), name + '_text'); + }); } function clearTransportPreference() { @@ -173,76 +178,6 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module return res; }; - - function withMocha(title, exports, defaultTimeout) { - describe(title, function () { - this.timeout(defaultTimeout || 60 * 1000); - - var counter = { - value: 0, - expected: -1, - }; - - function getTestApi (done) { - return { - done: function () { - if (counter.expected !== -1) { - chai.expect(counter.value).to.equal(counter.expected); - } - done(); - }, - ok: function (expression, message) { - counter.value += 1; - chai.expect(expression, message).to.be.ok; - }, - expect: function (expected) { - counter.expected = expected; - }, - equal: function (val1, val2, message) { - counter.value += 1; - chai.expect(val1).to.equal(val2, message); - }, - deepEqual: function (val1, val2, message) { - counter.value += 1; - chai.expect(val1).to.deep.equal(val2, message); - }, - notEqual: function (val1, val2, message) { - counter.value += 1; - chai.expect(val1).to.not.equal(val2, message); - }, - throws: function (fn) { - counter.value += 1; - chai.expect(fn).to.throw(); - } - } - }; - - before(function (done) { - if (typeof exports['before'] === 'function') { - exports['before'](getTestApi(done)); - } else { - done(); - } - }); - - beforeEach(function () { - counter.value = 0; - counter.expected = -1; - - clearTransportPreference(); - }) - - for (var testName in exports) { - (function(testName){ - if (testName !== 'before') { - it(testName, function (done) { - exports[testName](getTestApi(done)); - }) - }})(testName); - } - }) - } - return module.exports = { setupApp: testAppModule.setup, tearDownApp: testAppModule.tearDown, @@ -272,7 +207,6 @@ define(['spec/common/modules/testapp_module', 'spec/common/modules/client_module unroutableHost: unroutableHost, unroutableAddress: unroutableAddress, arrFind: arrFind, - arrFilter: arrFilter, - withMocha: withMocha + arrFilter: arrFilter }; }); diff --git a/spec/realtime/auth.test.js b/spec/realtime/auth.test.js index 8f0343918e..574384c2dd 100644 --- a/spec/realtime/auth.test.js +++ b/spec/realtime/auth.test.js @@ -1,1187 +1,1392 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var currentTime, exampleTokenDetails, exports = {}, - _exports = {}, - utils = helper.Utils, - displayError = helper.displayError, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection, - testOnAllTransports = helper.testOnAllTransports, - mixin = helper.Utils.mixin, - http = Ably.Rest.Http, - jwtTestChannelName = 'JWT_test' + String(Math.floor(Math.random() * 10000) + 1), - echoServer = "https://echo.ably.io"; +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var currentTime; + var exampleTokenDetails; + var exports = {}; + var expect = chai.expect; + var _exports = {}; + var utils = helper.Utils; + var displayError = helper.displayError; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; + var testOnAllTransports = helper.testOnAllTransports; + var mixin = helper.Utils.mixin; + var http = Ably.Rest.Http; + var jwtTestChannelName = 'JWT_test' + String(Math.floor(Math.random() * 10000) + 1); + var echoServer = 'https://echo.ably.io'; /* * Helper function to fetch JWT tokens from the echo server */ function getJWT(params, callback) { - var authUrl = echoServer + '/createJWT' - http.getUri(null, authUrl, null, params, function(err, body) { - if(err) { + var authUrl = echoServer + '/createJWT'; + http.getUri(null, authUrl, null, params, function (err, body) { + if (err) { callback(err, null); } callback(null, body.toString()); }); } - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } + describe('realtime/auth', function () { + this.timeout(60 * 1000); - var rest = helper.AblyRest({ queryTime: true }); - rest.time(function(err, time) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - currentTime = time; - rest.auth.requestToken({}, function(err, tokenDetails) { - test.ok(!err, err && displayError(err)); - test.done(); - }); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; } - }); - }); - }; - /* - * Base token generation case - */ - exports.authbase0 = function(test) { - test.expect(1); - var realtime = helper.AblyRealtime(); - realtime.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; - } - test.expect(5); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.equal(tokenDetails.expires, 60*60*1000 + tokenDetails.issued, 'Verify default expiry period'); - test.deepEqual(JSON.parse(tokenDetails.capability), {'*':['*']}, 'Verify token capability'); - closeAndFinish(test, realtime); + var rest = helper.AblyRest({ queryTime: true }); + rest.time(function (err, time) { + if (err) { + done(err); + return; + } else { + currentTime = time; + rest.auth.requestToken({}, function (err, tokenDetails) { + try { + expect(!err, err && displayError(err)).to.be.ok; + done(); + } catch (err) { + done(err); + } + }); + } + }); + }); }); - }; - /* - * Use authUrl for authentication with JSON TokenDetails response - */ - exports.auth_useAuthUrl_json = function(test) { - test.expect(1); - - var realtime, rest = helper.AblyRest(); - rest.auth.requestToken(null, null, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; - } - - var authPath = echoServer + "/?type=json&body=" + encodeURIComponent(JSON.stringify(tokenDetails)); - - realtime = helper.AblyRealtime({ authUrl: authPath }); - - realtime.connection.on('connected', function() { - test.ok(true, 'Connected to Ably using authUrl with TokenDetails JSON payload'); - closeAndFinish(test, realtime); - return; + /* + * Base token generation case + */ + it('authbase0', function (done) { + var realtime = helper.AblyRealtime(); + realtime.auth.requestToken(function (err, tokenDetails) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(tokenDetails.expires).to.equal(60 * 60 * 1000 + tokenDetails.issued, 'Verify default expiry period'); + expect(JSON.parse(tokenDetails.capability)).to.deep.equal({ '*': ['*'] }, 'Verify token capability'); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } }); - - monitorConnection(test, realtime); }); - }; - /* - * Use authUrl for authentication with JSON TokenDetails response, with authMethod=POST - */ - exports.auth_useAuthUrl_post_json = function(test) { - test.expect(1); - - var realtime, rest = helper.AblyRest(); - rest.auth.requestToken(null, null, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; - } + /* + * Use authUrl for authentication with JSON TokenDetails response + */ + it('auth_useAuthUrl_json', function (done) { + var realtime, + rest = helper.AblyRest(); + rest.auth.requestToken(null, null, function (err, tokenDetails) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - var authUrl = echoServer + "/?type=json&"; + var authPath = echoServer + '/?type=json&body=' + encodeURIComponent(JSON.stringify(tokenDetails)); - realtime = helper.AblyRealtime({ authUrl: authUrl, authMethod: "POST", authParams: tokenDetails}); + realtime = helper.AblyRealtime({ authUrl: authPath }); - realtime.connection.on('connected', function() { - test.ok(true, 'Connected to Ably using authUrl with TokenDetails JSON payload'); - closeAndFinish(test, realtime); - return; - }); + realtime.connection.on('connected', function () { + closeAndFinish(done, realtime); + return; + }); - monitorConnection(test, realtime); + monitorConnection(done, realtime); + }); }); - }; - /* - * Use authUrl for authentication with plain text token response - */ - exports.auth_useAuthUrl_plainText = function(test) { - test.expect(1); - - var realtime, rest = helper.AblyRest(); - rest.auth.requestToken(null, null, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; - } + /* + * Use authUrl for authentication with JSON TokenDetails response, with authMethod=POST + */ + it('auth_useAuthUrl_post_json', function (done) { + var realtime, + rest = helper.AblyRest(); + rest.auth.requestToken(null, null, function (err, tokenDetails) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - var authPath = echoServer + "/?type=text&body=" + tokenDetails['token']; + var authUrl = echoServer + '/?type=json&'; - realtime = helper.AblyRealtime({ authUrl: authPath }); + realtime = helper.AblyRealtime({ authUrl: authUrl, authMethod: 'POST', authParams: tokenDetails }); - realtime.connection.on('connected', function() { - test.ok(true, 'Connected to Ably using authUrl with TokenDetails JSON payload'); - closeAndFinish(test, realtime); - return; - }); + realtime.connection.on('connected', function () { + closeAndFinish(done, realtime); + return; + }); - monitorConnection(test, realtime); + monitorConnection(done, realtime); + }); }); - }; - /* - * Use authCallback for authentication with tokenRequest response - */ - exports.auth_useAuthCallback_tokenRequestResponse = function(test) { - test.expect(3); - - var realtime, rest = helper.AblyRest(); - var authCallback = function(tokenParams, callback) { - rest.auth.createTokenRequest(tokenParams, null, function(err, tokenRequest) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); + /* + * Use authUrl for authentication with plain text token response + */ + it('auth_useAuthUrl_plainText', function (done) { + var realtime, + rest = helper.AblyRest(); + rest.auth.requestToken(null, null, function (err, tokenDetails) { + if (err) { + closeAndFinish(done, realtime, err); return; } - test.ok("nonce" in tokenRequest); - callback(null, tokenRequest); - }); - }; - realtime = helper.AblyRealtime({ authCallback: authCallback }); + var authPath = echoServer + '/?type=text&body=' + tokenDetails['token']; + + realtime = helper.AblyRealtime({ authUrl: authPath }); + + realtime.connection.on('connected', function () { + closeAndFinish(done, realtime); + return; + }); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.method, 'token'); - test.ok(true, 'Connected to Ably using authCallback returning a TokenRequest'); - closeAndFinish(test, realtime); - return; + monitorConnection(done, realtime); + }); }); - monitorConnection(test, realtime); - }; + /* + * Use authCallback for authentication with tokenRequest response + */ + it('auth_useAuthCallback_tokenRequestResponse', function (done) { + var realtime, + rest = helper.AblyRest(); + var authCallback = function (tokenParams, callback) { + rest.auth.createTokenRequest(tokenParams, null, function (err, tokenRequest) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + try { + expect('nonce' in tokenRequest).to.be.ok; + } catch (err) { + done(err); + } + callback(null, tokenRequest); + }); + }; - /* - * Use authCallback for authentication with tokenDetails response, - * also check that clientId lib is initialized with is passed through - * to the auth callback - */ - exports.auth_useAuthCallback_tokenDetailsResponse = function(test) { - test.expect(4); - - var realtime, rest = helper.AblyRest(); - var clientId = "test clientid"; - var authCallback = function(tokenParams, callback) { - rest.auth.requestToken(tokenParams, null, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; + realtime = helper.AblyRealtime({ authCallback: authCallback }); + + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.method).to.equal('token'); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } - test.ok("token" in tokenDetails); - test.equal(tokenDetails.clientId, clientId); - callback(null, tokenDetails); }); - }; - realtime = helper.AblyRealtime({ authCallback: authCallback, clientId: clientId }); - - realtime.connection.on('connected', function() { - test.equal(realtime.auth.method, 'token'); - test.ok(true, 'Connected to Ably using authCallback returning a TokenRequest'); - closeAndFinish(test, realtime); - return; + monitorConnection(done, realtime); }); - monitorConnection(test, realtime); - }; + /* + * Use authCallback for authentication with tokenDetails response, + * also check that clientId lib is initialized with is passed through + * to the auth callback + */ + it('auth_useAuthCallback_tokenDetailsResponse', function (done) { + var realtime, + rest = helper.AblyRest(); + var clientId = 'test clientid'; + var authCallback = function (tokenParams, callback) { + rest.auth.requestToken(tokenParams, null, function (err, tokenDetails) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + try { + expect('token' in tokenDetails).to.be.ok; + expect(tokenDetails.clientId).to.equal(clientId); + } catch (err) { + done(err); + } + callback(null, tokenDetails); + }); + }; - /* - * Use authCallback for authentication with token string response - */ - exports.auth_useAuthCallback_tokenStringResponse = function(test) { - test.expect(3); - - var realtime, rest = helper.AblyRest(); - var authCallback = function(tokenParams, callback) { - rest.auth.requestToken(tokenParams, null, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; + realtime = helper.AblyRealtime({ authCallback: authCallback, clientId: clientId }); + + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.method).to.equal('token'); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } - test.ok("token" in tokenDetails); - callback(null, tokenDetails.token); }); - }; - realtime = helper.AblyRealtime({ authCallback: authCallback }); - - realtime.connection.on('connected', function() { - test.equal(realtime.auth.method, 'token'); - test.ok(true, 'Connected to Ably using authCallback returning a TokenRequest'); - closeAndFinish(test, realtime); - return; + monitorConnection(done, realtime); }); - monitorConnection(test, realtime); - }; - - /* - * RSA8c1c -- If the given authUrl includes any querystring params, they - * should be preserved, and in the GET case, authParams/tokenParams should be - * merged with them. If a name conflict occurs, authParams/tokenParams should - * take precedence - */ - exports.auth_useAuthUrl_mixed_authParams_qsParams = function(test) { - test.expect(1); - - var realtime, rest = helper.AblyRest(); - rest.auth.createTokenRequest(null, null, function(err, tokenRequest) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; - } - - /* Complete token request requires both parts to be combined, and - * requires the keyName in the higherPrecence part to take precedence - * over the wrong keyName */ - var lowerPrecedenceTokenRequestParts = { - keyName: "WRONG", - timestamp: tokenRequest.timestamp, - nonce: tokenRequest.nonce - }; - var higherPrecedenceTokenRequestParts = { - keyName: tokenRequest.keyName, - mac: tokenRequest.mac + /* + * Use authCallback for authentication with token string response + */ + it('auth_useAuthCallback_tokenStringResponse', function (done) { + var realtime, + rest = helper.AblyRest(); + var authCallback = function (tokenParams, callback) { + rest.auth.requestToken(tokenParams, null, function (err, tokenDetails) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + try { + expect('token' in tokenDetails).to.be.ok; + } catch (err) { + done(err); + } + callback(null, tokenDetails.token); + }); }; - var authPath = echoServer + "/qs_to_body" + utils.toQueryString(lowerPrecedenceTokenRequestParts); - realtime = helper.AblyRealtime({ authUrl: authPath, authParams: higherPrecedenceTokenRequestParts }); + realtime = helper.AblyRealtime({ authCallback: authCallback }); - realtime.connection.on('connected', function() { - test.ok(true, 'Connected to Ably'); - closeAndFinish(test, realtime); - return; + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.method).to.equal('token'); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } }); + + monitorConnection(done, realtime); }); - }; - /* - * Request a token using clientId, then initialize a connection without one, - * and check that the connection inherits the clientId from the tokenDetails - */ - exports.auth_clientid_inheritance = function(test) { - test.expect(1); - - var rest = helper.AblyRest(), - testClientId = 'testClientId'; - var authCallback = function(tokenParams, callback) { - rest.auth.requestToken({clientId: testClientId}, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + /* + * RSA8c1c -- If the given authUrl includes any querystring params, they + * should be preserved, and in the GET case, authParams/tokenParams should be + * merged with them. If a name conflict occurs, authParams/tokenParams should + * take precedence + */ + it('auth_useAuthUrl_mixed_authParams_qsParams', function (done) { + var realtime, + rest = helper.AblyRest(); + rest.auth.createTokenRequest(null, null, function (err, tokenRequest) { + if (err) { + closeAndFinish(done, realtime, err); return; } - callback(null, tokenDetails); - }); - }; - - var realtime = helper.AblyRealtime({ authCallback: authCallback }); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.clientId, testClientId); - realtime.connection.close(); - test.done(); - return; + /* Complete token request requires both parts to be combined, and + * requires the keyName in the higherPrecence part to take precedence + * over the wrong keyName */ + var lowerPrecedenceTokenRequestParts = { + keyName: 'WRONG', + timestamp: tokenRequest.timestamp, + nonce: tokenRequest.nonce + }; + var higherPrecedenceTokenRequestParts = { + keyName: tokenRequest.keyName, + mac: tokenRequest.mac + }; + var authPath = echoServer + '/qs_to_body' + utils.toQueryString(lowerPrecedenceTokenRequestParts); + + realtime = helper.AblyRealtime({ authUrl: authPath, authParams: higherPrecedenceTokenRequestParts }); + + realtime.connection.on('connected', function () { + closeAndFinish(done, realtime); + return; + }); + }); }); - realtime.connection.on('failed', function(err) { - realtime.close(); - test.ok(false, "Failed: " + displayError(err)); - test.done(); - return; - }); - }; + /* + * Request a token using clientId, then initialize a connection without one, + * and check that the connection inherits the clientId from the tokenDetails + */ + it('auth_clientid_inheritance', function (done) { + var rest = helper.AblyRest(), + testClientId = 'testClientId'; + var authCallback = function (tokenParams, callback) { + rest.auth.requestToken({ clientId: testClientId }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + callback(null, tokenDetails); + }); + }; - /* - * Rest token generation with clientId, then connecting with a - * different clientId, should fail with a library-generated message - * (RSA15a, RSA15c) - */ - exports.auth_clientid_inheritance2 = function(test) { - test.expect(3); - var clientRealtime, - testClientId = 'test client id'; - var rest = helper.AblyRest(); - rest.auth.requestToken({clientId:testClientId}, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + var realtime = helper.AblyRealtime({ authCallback: authCallback }); + + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.clientId).to.equal(testClientId); + realtime.connection.close(); + done(); + } catch (err) { + done(err); + } return; - } - clientRealtime = helper.AblyRealtime({token: tokenDetails, clientId: 'WRONG'}); - clientRealtime.connection.once('failed', function(stateChange){ - test.ok(true, 'Verify connection failed'); - test.equal(stateChange.reason.code, 80019); - test.equal(stateChange.reason.cause.code, 40102); - clientRealtime.close(); - test.done(); }); - }); - }; - /* - * Rest token generation with clientId '*', then connecting with just the - * token string and a different clientId, should succeed (RSA15b) - */ - exports.auth_clientid_inheritance3 = function(test) { - test.expect(1); - var realtime, - testClientId = 'test client id'; - var rest = helper.AblyRest(); - rest.auth.requestToken({clientId: '*'}, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - realtime = helper.AblyRealtime({token: tokenDetails.token, clientId: 'test client id'}); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.clientId, testClientId); - realtime.connection.close(); - test.done(); + realtime.connection.on('failed', function (err) { + realtime.close(); + done(err); return; }); - monitorConnection(test, realtime); }); - }; - /* - * Rest token generation with clientId '*', then connecting with - * tokenDetails and a clientId, should succeed (RSA15b) - */ - exports.auth_clientid_inheritance4 = function(test) { - test.expect(1); - var realtime, - testClientId = 'test client id'; - var rest = helper.AblyRest(); - rest.auth.requestToken({clientId: '*'}, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - realtime = helper.AblyRealtime({token: tokenDetails, clientId: 'test client id'}); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.clientId, testClientId); - realtime.connection.close(); - test.done(); - return; + /* + * Rest token generation with clientId, then connecting with a + * different clientId, should fail with a library-generated message + * (RSA15a, RSA15c) + */ + it('auth_clientid_inheritance2', function (done) { + var clientRealtime, + testClientId = 'test client id'; + var rest = helper.AblyRest(); + rest.auth.requestToken({ clientId: testClientId }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + clientRealtime = helper.AblyRealtime({ token: tokenDetails, clientId: 'WRONG' }); + clientRealtime.connection.once('failed', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(80019); + expect(stateChange.reason.cause.code).to.equal(40102); + clientRealtime.close(); + done(); + } catch (err) { + done(err); + } + }); }); - monitorConnection(test, realtime); }); - }; - /* - * Request a token using clientId, then initialize a connection using just the token string, - * and check that the connection inherits the clientId from the connectionDetails - */ - exports.auth_clientid_inheritance5 = function(test) { - test.expect(1); - var clientRealtime, - testClientId = 'test client id'; - var rest = helper.AblyRest(); - rest.auth.requestToken({clientId: testClientId}, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - clientRealtime = helper.AblyRealtime({token: tokenDetails.token}); - clientRealtime.connection.on('connected', function() { - test.equal(clientRealtime.auth.clientId, testClientId); - closeAndFinish(test, clientRealtime) - return; + /* + * Rest token generation with clientId '*', then connecting with just the + * token string and a different clientId, should succeed (RSA15b) + */ + it('auth_clientid_inheritance3', function (done) { + var realtime, + testClientId = 'test client id'; + var rest = helper.AblyRest(); + rest.auth.requestToken({ clientId: '*' }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + realtime = helper.AblyRealtime({ token: tokenDetails.token, clientId: 'test client id' }); + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.clientId).to.equal(testClientId); + realtime.connection.close(); + done(); + } catch (err) { + done(err); + } + return; + }); + monitorConnection(done, realtime); }); - monitorConnection(test, clientRealtime); }); - }; - /* RSA4c, RSA4e - * Try to connect with an authCallback that fails in various ways (calling back with an error, calling back with nothing, timing out, etc) should go to disconnected, not failed, and wrapped in a 80019 error code - */ - function authCallback_failures(realtimeOptions, expectFailure) { - return function(test) { - test.expect(3); - - var realtime = helper.AblyRealtime(realtimeOptions); - realtime.connection.on(function(stateChange) { - if(stateChange.previous !== 'initialized') { - if (helper.bestTransport === 'jsonp') { - // auth endpoints don't envelope, so we assume the 'least harmful' option, which is a disconnection with concomitant retry - test.equal(stateChange.current, 'disconnected', 'Check connection goes to the expected state'); - // jsonp doesn't let you examine the statuscode - test.equal(stateChange.reason.statusCode, 401, 'Check correct cause error code'); - } else { - test.equal(stateChange.current, expectFailure ? 'failed' : 'disconnected', 'Check connection goes to the expected state'); - test.equal(stateChange.reason.statusCode, expectFailure ? 403 : 401, 'Check correct cause error code'); - } - test.equal(stateChange.reason.code, 80019, 'Check correct error code'); - realtime.connection.off(); - closeAndFinish(test, realtime); + /* + * Rest token generation with clientId '*', then connecting with + * tokenDetails and a clientId, should succeed (RSA15b) + */ + it('auth_clientid_inheritance4', function (done) { + var realtime, + testClientId = 'test client id'; + var rest = helper.AblyRest(); + rest.auth.requestToken({ clientId: '*' }, function (err, tokenDetails) { + if (err) { + done(err); + return; } + realtime = helper.AblyRealtime({ token: tokenDetails, clientId: 'test client id' }); + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.clientId).to.equal(testClientId); + realtime.connection.close(); + done(); + } catch (err) { + done(err); + } + return; + }); + monitorConnection(done, realtime); }); - }; - } - - exports.authCallback_error = authCallback_failures({authCallback: function(tokenParams, callback) { - callback(new Error("An error from client code that the authCallback might return")); - }}); - - exports.authCallback_timeout = authCallback_failures({ - authCallback: function() { /* (^._.^)ノ */ }, - realtimeRequestTimeout: 100}); - - exports.authCallback_nothing = authCallback_failures({authCallback: function(tokenParams, callback) { - callback(); - }}); + }); - exports.authCallback_malformed = authCallback_failures({authCallback: function(tokenParams, callback) { - callback(null, { horse: 'ebooks' }); - }}); + /* + * Request a token using clientId, then initialize a connection using just the token string, + * and check that the connection inherits the clientId from the connectionDetails + */ + it('auth_clientid_inheritance5', function (done) { + var clientRealtime, + testClientId = 'test client id'; + var rest = helper.AblyRest(); + rest.auth.requestToken({ clientId: testClientId }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + clientRealtime = helper.AblyRealtime({ token: tokenDetails.token }); + clientRealtime.connection.on('connected', function () { + try { + expect(clientRealtime.auth.clientId).to.equal(testClientId); + closeAndFinish(done, clientRealtime); + } catch (err) { + closeAndFinish(done, clientRealtime, err); + } + return; + }); + monitorConnection(done, clientRealtime); + }); + }); - exports.authCallback_too_long_string = authCallback_failures({authCallback: function(tokenParams, callback) { - var token = ''; - for(var i=0; i 0, 'check that responseTime was +ve'); - closeAndFinish(test, realtime); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - exports.connectionAttributes = function(test) { - test.expect(6); - var realtime; - try { - realtime = helper.AblyRealtime({log: {level: 4}}); - realtime.connection.on('connected', function() { - test.equal(realtime.connection.serial, -1, "verify serial is -1 on connect"); - test.equal(realtime.connection.recoveryKey, realtime.connection.key + ':' + realtime.connection.serial + ':' + realtime.connection.connectionManager.msgSerial, 'verify correct recovery key'); + it('connectionPingWithCallback', function (done) { + var realtime; + try { + realtime = helper.AblyRealtime(); + realtime.connection.on('connected', function () { + realtime.connection.ping(function (err, responseTime) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + try { + expect(typeof responseTime).to.equal('number', 'check that a responseTime returned'); + expect(responseTime > 0, 'check that responseTime was +ve').to.be.ok; + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - var channel = realtime.channels.get('connectionattributes'); - channel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); + it('connectionAttributes', function (done) { + var realtime; + try { + realtime = helper.AblyRealtime({ log: { level: 4 } }); + realtime.connection.on('connected', function () { + try { + expect(realtime.connection.serial).to.equal(-1, 'verify serial is -1 on connect'); + expect(realtime.connection.recoveryKey).to.equal( + realtime.connection.key + + ':' + + realtime.connection.serial + + ':' + + realtime.connection.connectionManager.msgSerial, + 'verify correct recovery key' + ); + } catch (err) { + closeAndFinish(done, realtime, err); return; } - async.parallel([ - function(cb) { - channel.subscribe(function() { - setTimeout(function() { - test.equal(realtime.connection.serial, 0, "verify serial is 0 after message received") - if(realtime.connection.serial !== 0) { - var cm = realtime.connection.connectionManager; - console.log("connectionAttributes test: connection serial is " + realtime.connection.serial + "; active transport" + (cm.activeProtocol && cm.activeProtocol.transport && cm.activeProtocol.transport.shortName)) - } - test.equal(realtime.connection.recoveryKey, realtime.connection.key + ':' + realtime.connection.serial + ':' + realtime.connection.connectionManager.msgSerial, 'verify recovery key still correct'); - cb(); - }, 0); - }); - }, - function(cb) { - channel.publish("name", "data", cb); - test.equal(realtime.connection.serial, -1, "verify serial is -1 after publish begun but before message received") - } - ], function(err) { - if(err) { - test.ok(false, 'test failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); + + var channel = realtime.channels.get('connectionattributes'); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); return; } - realtime.connection.close(); - realtime.connection.whenState('closed', function() { - test.equal(realtime.connection.recoveryKey, null, 'verify recovery key null after close'); - closeAndFinish(test, realtime); - }); + async.parallel( + [ + function (cb) { + channel.subscribe(function () { + setTimeout(function () { + expect(realtime.connection.serial).to.equal(0, 'verify serial is 0 after message received'); + if (realtime.connection.serial !== 0) { + var cm = realtime.connection.connectionManager; + console.log( + 'connectionAttributes test: connection serial is ' + + realtime.connection.serial + + '; active transport' + + (cm.activeProtocol && cm.activeProtocol.transport && cm.activeProtocol.transport.shortName) + ); + } + expect(realtime.connection.recoveryKey).to.equal( + realtime.connection.key + + ':' + + realtime.connection.serial + + ':' + + realtime.connection.connectionManager.msgSerial, + 'verify recovery key still correct' + ); + cb(); + }, 0); + }); + }, + function (cb) { + channel.publish('name', 'data', cb); + expect(realtime.connection.serial).to.equal( + -1, + 'verify serial is -1 after publish begun but before message received' + ); + } + ], + function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + realtime.connection.close(); + realtime.connection.whenState('closed', function () { + try { + expect(realtime.connection.recoveryKey).to.equal(null, 'verify recovery key null after close'); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + } + ); }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - exports.unrecoverableConnection = function(test) { - test.expect(5); - var realtime, - fakeRecoveryKey = '_____!ablyjs_test_fake-key____:5:3'; - try { - realtime = helper.AblyRealtime({recover: fakeRecoveryKey}); - realtime.connection.on('connected', function(stateChange) { - test.equal(stateChange.reason.code, 80008, "verify unrecoverable-connection error set in stateChange.reason"); - test.equal(realtime.connection.errorReason.code, 80008, "verify unrecoverable-connection error set in connection.errorReason"); - test.equal(realtime.connection.serial, -1, "verify serial is -1 (new connection), not 5"); - test.equal(realtime.connection.connectionManager.msgSerial, 0, "verify msgSerial is 0 (new connection), not 3"); - test.equal(realtime.connection.key.indexOf('ablyjs_test_fake'), -1, "verify connection using a new connectionkey"); - closeAndFinish(test, realtime); - }); - } catch(e) { - test.ok(false, 'test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + it('unrecoverableConnection', function (done) { + var realtime, + fakeRecoveryKey = '_____!ablyjs_test_fake-key____:5:3'; + try { + realtime = helper.AblyRealtime({ recover: fakeRecoveryKey }); + realtime.connection.on('connected', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal( + 80008, + 'verify unrecoverable-connection error set in stateChange.reason' + ); + expect(realtime.connection.errorReason.code).to.equal( + 80008, + 'verify unrecoverable-connection error set in connection.errorReason' + ); + expect(realtime.connection.serial).to.equal(-1, 'verify serial is -1 (new connection), not 5'); + expect(realtime.connection.connectionManager.msgSerial).to.equal( + 0, + 'verify msgSerial is 0 (new connection), not 3' + ); + expect(realtime.connection.key.indexOf('ablyjs_test_fake')).to.equal( + -1, + 'verify connection using a new connectionkey' + ); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* - * Check that a message published on one transport that has not yet been - * acked will be republished with the same msgSerial on a new transport (eg - * after a resume or an upgrade), before any new messages are send (and - * without being merged with new messages) - */ - exports.connectionQueuing = function(test) { - test.expect(5); - var realtime = helper.AblyRealtime({transports: [helper.bestTransport]}), - channel = realtime.channels.get('connectionQueuing'), - connectionManager = realtime.connection.connectionManager; + /* + * Check that a message published on one transport that has not yet been + * acked will be republished with the same msgSerial on a new transport (eg + * after a resume or an upgrade), before any new messages are send (and + * without being merged with new messages) + */ + it('connectionQueuing', function (done) { + var realtime = helper.AblyRealtime({ transports: [helper.bestTransport] }), + channel = realtime.channels.get('connectionQueuing'), + connectionManager = realtime.connection.connectionManager; - realtime.connection.once('connected', function() { - var transport = connectionManager.activeProtocol.transport; - channel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + helper.displayError(err)); - closeAndFinish(test, realtime); - return; - } - /* Sabotage sending the message */ - transport.send = function(msg) { - if(msg.action == 15) { - test.equal(msg.msgSerial, 0, 'Expect msgSerial to be 0'); + realtime.connection.once('connected', function () { + var transport = connectionManager.activeProtocol.transport; + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - }; - - async.parallel([ - function(cb) { - /* Sabotaged publish */ - channel.publish('first', null, function(err) { - test.ok(!err, "Check publish happened (eventually) without err"); - cb(); - }); - }, - function(cb) { - /* After the disconnect, on reconnect, spy on transport.send again */ - connectionManager.once('transport.pending', function(transport) { - var oldSend = transport.send; + /* Sabotage sending the message */ + transport.send = function (msg) { + if (msg.action == 15) { + expect(msg.msgSerial).to.equal(0, 'Expect msgSerial to be 0'); + } + }; - transport.send = function(msg, msgCb) { - if(msg.action === 15) { - if(msg.messages[0].name === 'first') { - test.equal(msg.msgSerial, 0, 'Expect msgSerial of original message to still be 0'); - test.equal(msg.messages.length, 1, 'Expect second message to not have been merged with the attempted message'); - } else if(msg.messages[0].name === 'second') { - test.equal(msg.msgSerial, 1, 'Expect msgSerial of new message to be 1'); - cb(); + async.parallel( + [ + function (cb) { + /* Sabotaged publish */ + channel.publish('first', null, function (err) { + try { + expect(!err, 'Check publish happened (eventually) without err').to.be.ok; + } catch (err) { + cb(err); + return; } - } - oldSend.call(transport, msg, msgCb); - }; - channel.publish('second', null); - }); + cb(); + }); + }, + function (cb) { + /* After the disconnect, on reconnect, spy on transport.send again */ + connectionManager.once('transport.pending', function (transport) { + var oldSend = transport.send; - /* Disconnect the transport (will automatically reconnect and resume) () */ - connectionManager.disconnectAllTransports(); - } - ], function() { - closeAndFinish(test, realtime); - }); + transport.send = function (msg, msgCb) { + if (msg.action === 15) { + if (msg.messages[0].name === 'first') { + try { + expect(msg.msgSerial).to.equal(0, 'Expect msgSerial of original message to still be 0'); + expect(msg.messages.length).to.equal( + 1, + 'Expect second message to not have been merged with the attempted message' + ); + } catch (err) { + cb(err); + return; + } + } else if (msg.messages[0].name === 'second') { + try { + expect(msg.msgSerial).to.equal(1, 'Expect msgSerial of new message to be 1'); + } catch (err) { + cb(err); + return; + } + cb(); + } + } + oldSend.call(transport, msg, msgCb); + }; + channel.publish('second', null); + }); + /* Disconnect the transport (will automatically reconnect and resume) () */ + connectionManager.disconnectAllTransports(); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }); }); }); - }; - /* - * Inject a new CONNECTED with different connectionDetails; check they're used - */ - exports.connectionDetails = function(test) { - test.expect(5); - var realtime = helper.AblyRealtime(), - connectionManager = realtime.connection.connectionManager; - realtime.connection.once('connected', function() { - connectionManager.once('connectiondetails', function(details) { - test.equal(details.connectionStateTtl, 12345, 'Check connectionStateTtl in event'); - test.equal(connectionManager.connectionStateTtl, 12345, 'Check connectionStateTtl set in connectionManager'); - test.equal(details.clientId, 'foo', 'Check clientId in event'); - test.equal(realtime.auth.clientId, 'foo', 'Check clientId set in auth'); - test.equal(realtime.options.maxMessageSize, 98765, 'Check maxMessageSize set'); - closeAndFinish(test, realtime); + /* + * Inject a new CONNECTED with different connectionDetails; check they're used + */ + it('connectionDetails', function (done) { + var realtime = helper.AblyRealtime(), + connectionManager = realtime.connection.connectionManager; + realtime.connection.once('connected', function () { + connectionManager.once('connectiondetails', function (details) { + try { + expect(details.connectionStateTtl).to.equal(12345, 'Check connectionStateTtl in event'); + expect(connectionManager.connectionStateTtl).to.equal( + 12345, + 'Check connectionStateTtl set in connectionManager' + ); + expect(details.clientId).to.equal('foo', 'Check clientId in event'); + expect(realtime.auth.clientId).to.equal('foo', 'Check clientId set in auth'); + expect(realtime.options.maxMessageSize).to.equal(98765, 'Check maxMessageSize set'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 4, + connectionId: 'a', + connectionKey: 'ab', + connectionSerial: -1, + connectionDetails: { + clientId: 'foo', + maxMessageSize: 98765, + connectionStateTtl: 12345 + } + }) + ); }); - connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 4, - connectionId: 'a', - connectionKey: 'ab', - connectionSerial: -1, - connectionDetails: { - clientId: 'foo', - maxMessageSize: 98765, - connectionStateTtl: 12345 - } - })); + monitorConnection(done, realtime); }); - monitorConnection(test, realtime); - }; + }); +}); - helper.withMocha('realtime/connection', exports); -}); \ No newline at end of file diff --git a/spec/realtime/connectivity.test.js b/spec/realtime/connectivity.test.js index 1fa34d8e38..2d9bd39053 100644 --- a/spec/realtime/connectivity.test.js +++ b/spec/realtime/connectivity.test.js @@ -1,33 +1,37 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection, - utils = helper.Utils; +define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; + var utils = helper.Utils; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); + describe('realtime/connectivity', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); }); - }; - /* - * Connect with available http transports; internet connectivity check should work - */ - exports.http_connectivity_check = function(test) { - test.expect(1); - Ably.Realtime.Http.checkConnectivity(function(err, res) { - test.ok(res && !err, 'Connectivity check completed ' + (err && utils.inspectError(err))); - test.done(); - }) - }; + /* + * Connect with available http transports; internet connectivity check should work + */ + it('http_connectivity_check', function (done) { + Ably.Realtime.Http.checkConnectivity(function (err, res) { + try { + expect(res && !err, 'Connectivity check completed ' + (err && utils.inspectError(err))).to.be.ok; + } catch (err) { + done(err); + return; + } + done(); + }); + }); + }); +}); - helper.withMocha('realtime/connectivity', exports); -}); \ No newline at end of file diff --git a/spec/realtime/crypto.test.js b/spec/realtime/crypto.test.js index 388400d00a..b23ccf1e08 100644 --- a/spec/realtime/crypto.test.js +++ b/spec/realtime/crypto.test.js @@ -1,711 +1,865 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - loadTestData = helper.loadTestData, - BufferUtils = Ably.Realtime.BufferUtils, - Crypto = Ably.Realtime.Crypto, - Message = Ably.Realtime.Message, - displayError = helper.displayError, - testResourcesPath = helper.testResourcesPath, - msgpack = (typeof(window) == 'object') ? Ably.msgpack : require('@ably/msgpack-js'), - testOnAllTransports = helper.testOnAllTransports, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection; +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + var loadTestData = helper.loadTestData; + var BufferUtils = Ably.Realtime.BufferUtils; + var Crypto = Ably.Realtime.Crypto; + var Message = Ably.Realtime.Message; + var displayError = helper.displayError; + var testResourcesPath = helper.testResourcesPath; + var msgpack = typeof window == 'object' ? Ably.msgpack : require('@ably/msgpack-js'); + var testOnAllTransports = helper.testOnAllTransports; + var closeAndFinish = helper.closeAndFinish; function attachChannels(channels, callback) { - async.map(channels, function(channel, cb) { channel.attach(cb); }, callback); + async.map( + channels, + function (channel, cb) { + channel.attach(cb); + }, + callback + ); } - function testMessageEquality(test, one, two) { - // treat `null` same as `undefined` (using ==, rather than ===) - test.ok(one.encoding == two.encoding, "Encoding mismatch ('" + one.encoding + "' != '" + two.encoding + "')."); + function testMessageEquality(done, one, two) { + try { + // treat `null` same as `undefined` (using ==, rather than ===) + expect(one.encoding == two.encoding, "Encoding mismatch ('" + one.encoding + "' != '" + two.encoding + "').").to + .be.ok; - if(typeof(one.data) === 'string' && typeof(two.data) === 'string') { - test.ok(one.data === two.data, "String data contents mismatch."); - return; - } + if (typeof one.data === 'string' && typeof two.data === 'string') { + expect(one.data === two.data, 'String data contents mismatch.').to.be.ok; + return; + } - if(BufferUtils.isBuffer(one.data) && BufferUtils.isBuffer(two.data)) { - test.ok(BufferUtils.bufferCompare(one.data, two.data) === 0, "Buffer data contents mismatch."); - return; - } + if (BufferUtils.isBuffer(one.data) && BufferUtils.isBuffer(two.data)) { + expect(BufferUtils.bufferCompare(one.data, two.data) === 0, 'Buffer data contents mismatch.').to.be.ok; + return; + } - var json1 = JSON.stringify(one.data); - var json2 = JSON.stringify(two.data); - if (null === json1 || undefined === json1 || null === json2 || undefined === json2) { - test.ok(false, "JSON stringify failed."); - return; + var json1 = JSON.stringify(one.data); + var json2 = JSON.stringify(two.data); + if (null === json1 || undefined === json1 || null === json2 || undefined === json2) { + expect(false, 'JSON stringify failed.').to.be.ok; + return; + } + expect(json1 === json2, 'JSON data contents mismatch.').to.be.ok; + } catch (err) { + done(err); } - test.ok(json1 === json2, "JSON data contents mismatch."); } - function testEachFixture(test, filename, channelName, testsPerFixture, fixtureTest) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); + function testEachFixture(done, filename, channelName, testsPerFixture, fixtureTest) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - loadTestData(testResourcesPath + filename, function(err, testData) { - if(err) { - test.ok(false, 'Unable to get test assets; err = ' + displayError(err)); + loadTestData(testResourcesPath + filename, function (err, testData) { + if (err) { + done(new Error('Unable to get test assets; err = ' + displayError(err))); return; } var realtime = helper.AblyRealtime(); var key = BufferUtils.base64Decode(testData.key); var iv = BufferUtils.base64Decode(testData.iv); - var channel = realtime.channels.get(channelName, {cipher: {key: key, iv: iv}}); - - test.expect(testData.items.length * testsPerFixture); - for(var i = 0; i < testData.items.length; i++) { - var item = testData.items[i]; - - /* read messages from test data and decode (ie remove any base64 encoding). */ - var testMessage = Message.fromEncoded(item.encoded); - var encryptedMessage = Message.fromEncoded(item.encrypted); - /* reset channel cipher, to ensure it uses the given iv */ - channel.setOptions({cipher: {key: key, iv: iv}}); - fixtureTest(channel.channelOptions, testMessage, encryptedMessage, item.msgpack); - } - closeAndFinish(test, realtime); - }); - } + var channel = realtime.channels.get(channelName, { cipher: { key: key, iv: iv } }); - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); + try { + for (var i = 0; i < testData.items.length; i++) { + var item = testData.items[i]; + + /* read messages from test data and decode (ie remove any base64 encoding). */ + var testMessage = Message.fromEncoded(item.encoded); + var encryptedMessage = Message.fromEncoded(item.encrypted); + /* reset channel cipher, to ensure it uses the given iv */ + channel.setOptions({ cipher: { key: key, iv: iv } }); + fixtureTest(channel.channelOptions, testMessage, encryptedMessage, item.msgpack); + } + } catch (err) { + closeAndFinish(done, realtime, err); + return; } - test.ok(true, 'app set up'); - test.done(); + closeAndFinish(done, realtime); }); - }; - - /* generateRandomKey with an explicit keyLength */ - exports.generateRandomKey0 = function(test) { - test.expect(1); - Crypto.generateRandomKey(64, function(err, key) { - if(err) test.ok(false, helper.displayError(err)); - /* .length for a nodejs buffer, .sigbytes for a browser CryptoJS WordArray */ - test.equal(key.length || key.sigBytes, 8, "generated key is the correct length"); - test.done(); - }) } - /* generateRandomKey with no keyLength should generate 256-bit keys */ - exports.generateRandomKey1 = function(test) { - test.expect(1); - Crypto.generateRandomKey(function(err, key) { - if(err) test.ok(false, helper.displayError(err)); - test.equal(key.length || key.sigBytes, 32, "generated key is the default length"); - test.done(); - }) - } + describe('realtime/crypto', function () { + this.timeout(60 * 1000); - exports.getDefaultParams_wordArray_key = function(test) { - test.expect(3); - Crypto.generateRandomKey(function(err, key) { - if(err) test.ok(false, helper.displayError(err)); - var params = Crypto.getDefaultParams({key: key}); - test.equal(params.key, key); - test.equal(params.algorithm, 'aes', 'check default algorithm'); - test.equal(params.mode, 'cbc', 'check default mode'); - test.done(); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); }); - } - exports.getDefaultParams_base64_key = function(test) { - test.expect(1); - Crypto.generateRandomKey(function(err, key) { - if(err) test.ok(false, helper.displayError(err)); - var b64key = Ably.Realtime.BufferUtils.base64Encode(key); - var params = Crypto.getDefaultParams({key: b64key}); - test.equal(BufferUtils.bufferCompare(params.key, key), 0); - test.done(); + /* generateRandomKey with an explicit keyLength */ + it('generateRandomKey0', function (done) { + Crypto.generateRandomKey(64, function (err, key) { + if (err) { + done(err); + return; + } + try { + /* .length for a nodejs buffer, .sigbytes for a browser CryptoJS WordArray */ + expect(key.length || key.sigBytes).to.equal(8, 'generated key is the correct length'); + done(); + } catch (err) { + done(err); + } + }); }); - } - exports.getDefaultParams_check_keylength = function(test) { - test.expect(1); - Crypto.generateRandomKey(64, function(err, key) { - if(err) test.ok(false, helper.displayError(err)); - try { - Crypto.getDefaultParams({key: key}); - } catch(e) { - test.ok(true, 'getDefaultParams with a 64-bit key threw an exception'); - test.done(); - } + /* generateRandomKey with no keyLength should generate 256-bit keys */ + it('generateRandomKey1', function (done) { + Crypto.generateRandomKey(function (err, key) { + if (err) { + done(err); + return; + } + try { + expect(key.length || key.sigBytes).to.equal(32, 'generated key is the default length'); + done(); + } catch (err) { + done(err); + } + }); }); - } - exports.getDefaultParams_preserves_custom_algorithms = function(test) { - test.expect(4); - Crypto.generateRandomKey(64, function(err, key) { - if(err) test.ok(false, helper.displayError(err)); - try { - var params = Crypto.getDefaultParams({key: key, algorithm: 'foo', mode: 'bar'}); - test.equal(params.key, key); - test.equal(params.algorithm, 'foo'); - test.equal(params.mode, 'bar'); - test.equal(params.keyLength, 64); - test.done(); - } catch(e) { - test.ok(false, 'getDefaultParams should not have thrown exception ' + e +' as it doesn’t recognise the algorithm'); - } + it('getDefaultParams_wordArray_key', function (done) { + Crypto.generateRandomKey(function (err, key) { + if (err) { + done(err); + } + var params = Crypto.getDefaultParams({ key: key }); + try { + expect(params.key).to.equal(key); + expect(params.algorithm).to.equal('aes', 'check default algorithm'); + expect(params.mode).to.equal('cbc', 'check default mode'); + done(); + } catch (err) { + done(err); + } + }); }); - } - exports.encrypt_message_128 = function(test) { - testEachFixture(test, 'crypto-data-128.json', 'encrypt_message_128', 2, function(channelOpts, testMessage, encryptedMessage) { - /* encrypt plaintext message; encode() also to handle data that is not already string or buffer */ - Message.encode(testMessage, channelOpts, function() { - /* compare */ - testMessageEquality(test, testMessage, encryptedMessage); + it('getDefaultParams_base64_key', function (done) { + Crypto.generateRandomKey(function (err, key) { + if (err) { + done(err); + return; + } + var b64key = Ably.Realtime.BufferUtils.base64Encode(key); + var params = Crypto.getDefaultParams({ key: b64key }); + try { + expect(BufferUtils.bufferCompare(params.key, key)).to.equal(0); + done(); + } catch (err) { + done(err); + } }); }); - }; - - exports.encrypt_message_256 = function(test) { - testEachFixture(test, 'crypto-data-256.json', 'encrypt_message_256', 2, function(channelOpts, testMessage, encryptedMessage) { - /* encrypt plaintext message; encode() also to handle data that is not already string or buffer */ - Message.encode(testMessage, channelOpts, function() { - /* compare */ - testMessageEquality(test, testMessage, encryptedMessage); + + it('getDefaultParams_check_keylength', function (done) { + Crypto.generateRandomKey(64, function (err, key) { + if (err) { + done(err); + return; + } + try { + Crypto.getDefaultParams({ key: key }); + done(new Error('expected getDefaultParams with a 64-bit key to throw an exception')); + } catch (err) { + done(); + } }); }); - }; - - exports.decrypt_message_128 = function(test) { - testEachFixture(test, 'crypto-data-128.json', 'decrypt_message_128', 2, function(channelOpts, testMessage, encryptedMessage) { - /* decrypt encrypted message; decode() also to handle data that is not string or buffer */ - Message.decode(encryptedMessage, channelOpts); - /* compare */ - testMessageEquality(test, testMessage, encryptedMessage); - }); - }; - - exports.decrypt_message_256 = function(test) { - testEachFixture(test, 'crypto-data-256.json', 'decrypt_message_256', 2, function(channelOpts, testMessage, encryptedMessage) { - /* decrypt encrypted message; decode() also to handle data that is not string or buffer */ - Message.decode(encryptedMessage, channelOpts); - /* compare */ - testMessageEquality(test, testMessage, encryptedMessage); - }); - }; - exports.fromEncoded_cipher_options = function(test) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } + it('getDefaultParams_preserves_custom_algorithms', function (done) { + Crypto.generateRandomKey(64, function (err, key) { + if (err) { + done(err); + return; + } + try { + var params = Crypto.getDefaultParams({ key: key, algorithm: 'foo', mode: 'bar' }); + expect(params.key).to.equal(key); + expect(params.algorithm).to.equal('foo'); + expect(params.mode).to.equal('bar'); + expect(params.keyLength).to.equal(64); + done(); + } catch (err) { + done(err); + } + }); + }); - loadTestData(testResourcesPath + 'crypto-data-256.json', function(err, testData) { - if(err) { - test.ok(false, 'Unable to get test assets; err = ' + displayError(err)); - return; - } - var key = BufferUtils.base64Decode(testData.key); - var iv = BufferUtils.base64Decode(testData.iv); + it('encrypt_message_128', function (done) { + testEachFixture( + done, + 'crypto-data-128.json', + 'encrypt_message_128', + 2, + function (channelOpts, testMessage, encryptedMessage) { + /* encrypt plaintext message; encode() also to handle data that is not already string or buffer */ + Message.encode(testMessage, channelOpts, function () { + /* compare */ + testMessageEquality(done, testMessage, encryptedMessage); + }); + } + ); + }); - test.expect(testData.items.length * 2); - for(var i = 0; i < testData.items.length; i++) { - var item = testData.items[i]; - var testMessage = Message.fromEncoded(item.encoded); - var decryptedMessage = Message.fromEncoded(item.encrypted, {cipher: {key: key, iv: iv}}); - testMessageEquality(test, testMessage, decryptedMessage); - } - test.done(); + it('encrypt_message_256', function (done) { + testEachFixture( + done, + 'crypto-data-256.json', + 'encrypt_message_256', + 2, + function (channelOpts, testMessage, encryptedMessage) { + /* encrypt plaintext message; encode() also to handle data that is not already string or buffer */ + Message.encode(testMessage, channelOpts, function () { + /* compare */ + testMessageEquality(done, testMessage, encryptedMessage); + }); + } + ); }); - }; - exports.msgpack_128 = function(test) { - if(typeof ArrayBuffer === 'undefined') { - /* Encryption or binary transport not supported */ - test.done(); - return; - } + it('decrypt_message_128', function (done) { + testEachFixture( + done, + 'crypto-data-128.json', + 'decrypt_message_128', + 2, + function (channelOpts, testMessage, encryptedMessage) { + /* decrypt encrypted message; decode() also to handle data that is not string or buffer */ + Message.decode(encryptedMessage, channelOpts); + /* compare */ + testMessageEquality(done, testMessage, encryptedMessage); + } + ); + }); - testEachFixture(test, 'crypto-data-128.json', 'msgpack_128', 2, function(channelOpts, testMessage, encryptedMessage, msgpackEncodedMessage) { - Message.encode(testMessage, channelOpts, function() { - var msgpackFromEncoded = msgpack.encode(testMessage); - var msgpackFromEncrypted = msgpack.encode(encryptedMessage); - var messageFromMsgpack = Message.fromValues(msgpack.decode(BufferUtils.base64Decode(msgpackEncodedMessage))); + it('decrypt_message_256', function (done) { + testEachFixture( + done, + 'crypto-data-256.json', + 'decrypt_message_256', + 2, + function (channelOpts, testMessage, encryptedMessage) { + /* decrypt encrypted message; decode() also to handle data that is not string or buffer */ + Message.decode(encryptedMessage, channelOpts); + /* compare */ + testMessageEquality(done, testMessage, encryptedMessage); + } + ); + }); - /* Mainly testing that we're correctly encoding the direct output from - * CryptoJS (a wordArray) into the msgpack binary type */ - test.equal(BufferUtils.bufferCompare(msgpackFromEncoded, msgpackFromEncrypted), 0, 'verify msgpack encodings of newly-encrypted and preencrypted messages identical using bufferCompare'); + it('fromEncoded_cipher_options', function (done) { + if (!Crypto) { + done(new Error('Encryption not supported')); + return; + } - /* Can't compare msgpackFromEncoded with fixture data because can't - * assume key order in the msgpack serialisation. So test decoded instead */ - test.deepEqual(messageFromMsgpack, encryptedMessage, 'verify msgpack fixture decodes correctly'); + loadTestData(testResourcesPath + 'crypto-data-256.json', function (err, testData) { + if (err) { + done(err); + return; + } + var key = BufferUtils.base64Decode(testData.key); + var iv = BufferUtils.base64Decode(testData.iv); + + for (var i = 0; i < testData.items.length; i++) { + var item = testData.items[i]; + var testMessage = Message.fromEncoded(item.encoded); + var decryptedMessage = Message.fromEncoded(item.encrypted, { cipher: { key: key, iv: iv } }); + testMessageEquality(done, testMessage, decryptedMessage); + } + done(); }); }); - }; - exports.msgpack_256 = function(test) { - if(typeof ArrayBuffer === 'undefined') { - /* Encryption or binary transport not supported */ - test.done(); - return; - } - - testEachFixture(test, 'crypto-data-256.json', 'msgpack_256', 2, function(channelOpts, testMessage, encryptedMessage, msgpackEncodedMessage) { - Message.encode(testMessage, channelOpts, function() { - var msgpackFromEncoded = msgpack.encode(testMessage); - var msgpackFromEncrypted = msgpack.encode(encryptedMessage); - var messageFromMsgpack = Message.fromValues(msgpack.decode(BufferUtils.base64Decode(msgpackEncodedMessage))); - - /* Mainly testing that we're correctly encoding the direct output from - * CryptoJS (a wordArray) into the msgpack binary type */ - test.equal(BufferUtils.bufferCompare(msgpackFromEncoded, msgpackFromEncrypted), 0, 'verify msgpack encodings of newly-encrypted and preencrypted messages identical using bufferCompare'); - - /* Can't compare msgpackFromEncoded with fixture data because can't - * assume key order in the msgpack serialisation. So test decoded instead */ - test.deepEqual(messageFromMsgpack, encryptedMessage, 'verify msgpack fixture decodes correctly'); + /* Tests require encryption and binary transport */ + if (typeof ArrayBuffer !== 'undefined') { + it('msgpack_128', function (done) { + testEachFixture( + done, + 'crypto-data-128.json', + 'msgpack_128', + 2, + function (channelOpts, testMessage, encryptedMessage, msgpackEncodedMessage) { + Message.encode(testMessage, channelOpts, function () { + var msgpackFromEncoded = msgpack.encode(testMessage); + var msgpackFromEncrypted = msgpack.encode(encryptedMessage); + var messageFromMsgpack = Message.fromValues( + msgpack.decode(BufferUtils.base64Decode(msgpackEncodedMessage)) + ); + + try { + /* Mainly testing that we're correctly encoding the direct output from + * CryptoJS (a wordArray) into the msgpack binary type */ + expect(BufferUtils.bufferCompare(msgpackFromEncoded, msgpackFromEncrypted)).to.equal( + 0, + 'verify msgpack encodings of newly-encrypted and preencrypted messages identical using bufferCompare' + ); + + /* Can't compare msgpackFromEncoded with fixture data because can't + * assume key order in the msgpack serialisation. So test decoded instead */ + expect(messageFromMsgpack).to.deep.equal(encryptedMessage, 'verify msgpack fixture decodes correctly'); + } catch (err) { + done(err); + } + }); + } + ); }); - }); - }; - function single_send(test, realtimeOpts, keyLength) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; + it('msgpack_256', function (done) { + testEachFixture( + done, + 'crypto-data-256.json', + 'msgpack_256', + 2, + function (channelOpts, testMessage, encryptedMessage, msgpackEncodedMessage) { + Message.encode(testMessage, channelOpts, function () { + var msgpackFromEncoded = msgpack.encode(testMessage); + var msgpackFromEncrypted = msgpack.encode(encryptedMessage); + var messageFromMsgpack = Message.fromValues( + msgpack.decode(BufferUtils.base64Decode(msgpackEncodedMessage)) + ); + + try { + /* Mainly testing that we're correctly encoding the direct output from + * CryptoJS (a wordArray) into the msgpack binary type */ + expect(BufferUtils.bufferCompare(msgpackFromEncoded, msgpackFromEncrypted)).to.equal( + 0, + 'verify msgpack encodings of newly-encrypted and preencrypted messages identical using bufferCompare' + ); + + /* Can't compare msgpackFromEncoded with fixture data because can't + * assume key order in the msgpack serialisation. So test decoded instead */ + expect(messageFromMsgpack).to.deep.equal(encryptedMessage, 'verify msgpack fixture decodes correctly'); + } catch (err) { + done(err); + } + }); + } + ); + }); } - test.expect(3); - Crypto.generateRandomKey(keyLength, function(err, key) { - if(err) { - test.ok(false, 'Unable to generate key; err = ' + displayError(err)); - closeAndFinish(test, realtime); + function single_send(done, realtimeOpts, keyLength) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - /* For single_send tests we test the 'shortcut' way of setting the cipher - * in channels.get. No callback, but that's ok even for node which has - * async iv generation since the publish is on an attach cb */ - var realtime = helper.AblyRealtime(realtimeOpts), - channel = realtime.channels.get('single_send', {cipher: {key: key}}), - messageText = 'Test message for single_send - ' + JSON.stringify(realtimeOpts); - - channel.attach(function(err) { - if(err) { - test.ok(false, 'Unable to attach; err = ' + displayError(err)); - closeAndFinish(test, realtime); + + Crypto.generateRandomKey(keyLength, function (err, key) { + if (err) { + closeAndFinish(done, realtime, err); return; } - test.equal(channel.channelOptions.cipher.algorithm, 'aes'); - test.equal(channel.channelOptions.cipher.keyLength, keyLength); - channel.subscribe('event0', function(msg) { - test.ok(msg.data == messageText); - closeAndFinish(test, realtime); + /* For single_send tests we test the 'shortcut' way of setting the cipher + * in channels.get. No callback, but that's ok even for node which has + * async iv generation since the publish is on an attach cb */ + var realtime = helper.AblyRealtime(realtimeOpts), + channel = realtime.channels.get('single_send', { cipher: { key: key } }), + messageText = 'Test message for single_send - ' + JSON.stringify(realtimeOpts); + + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + try { + expect(channel.channelOptions.cipher.algorithm).to.equal('aes'); + expect(channel.channelOptions.cipher.keyLength).to.equal(keyLength); + } catch (err) { + closeAndFinish(done, realtime, err); + } + channel.subscribe('event0', function (msg) { + try { + expect(msg.data == messageText).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + channel.publish('event0', messageText); }); - channel.publish('event0', messageText); - }) + }); + } + + /** + * Publish and subscribe, various transport, 128 and 256-bit + */ + testOnAllTransports('single_send_128', function (realtimeOpts) { + return function (done) { + single_send(done, realtimeOpts, 128); + }; }); - } - /** - * Publish and subscribe, various transport, 128 and 256-bit - */ - testOnAllTransports(exports, 'single_send_128', function(realtimeOpts) { return function(test) { - single_send(test, realtimeOpts, 128); - }}) - - testOnAllTransports(exports, 'single_send_256', function(realtimeOpts) { return function(test) { - single_send(test, realtimeOpts, 256); - }}) - - function _multiple_send(test, text, iterations, delay) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } + testOnAllTransports('single_send_256', function (realtimeOpts) { + return function (done) { + single_send(done, realtimeOpts, 256); + }; + }); - var realtime = helper.AblyRealtime({ useBinaryProtocol: !text}); - test.expect(iterations + 3); - var channelName = 'multiple_send_' + (text ? 'text_' : 'binary_') + iterations + '_' + delay, - channel = realtime.channels.get(channelName), - messageText = 'Test message (' + channelName + ')'; - - Crypto.generateRandomKey(128, function(err, key) { - channel.setOptions({cipher: {key: key}}); - test.equal(channel.channelOptions.cipher.algorithm, 'aes'); - test.equal(channel.channelOptions.cipher.keyLength, 128); - function sendAll(sendCb) { - var sent = 0; - var sendOnce = function() { - channel.publish('event0', messageText); - if(++sent == iterations) { - sendCb(null); - return; - } - setTimeout(sendOnce, delay); - }; - sendOnce(); - } - function recvAll(recvCb) { - var received = 0; - channel.subscribe('event0', function(msg) { - test.ok(msg.data == messageText); - if(++received == iterations) - recvCb(null); - }); + function _multiple_send(done, text, iterations, delay) { + if (!Crypto) { + done(new Error('Encryption not supported')); + return; } - channel.attach(function(err) { - if(err) { - test.ok(false, 'Unable to attach; err = ' + displayError(err)); - closeAndFinish(test, realtime); + var realtime = helper.AblyRealtime({ useBinaryProtocol: !text }); + var channelName = 'multiple_send_' + (text ? 'text_' : 'binary_') + iterations + '_' + delay, + channel = realtime.channels.get(channelName), + messageText = 'Test message (' + channelName + ')'; + + Crypto.generateRandomKey(128, function (err, key) { + channel.setOptions({ cipher: { key: key } }); + try { + expect(channel.channelOptions.cipher.algorithm).to.equal('aes'); + expect(channel.channelOptions.cipher.keyLength).to.equal(128); + } catch (err) { + closeAndFinish(done, realtime, err); return; } - async.parallel([sendAll, recvAll], function(err) { - if(err) { - test.ok(false, 'Error sending messages; err = ' + displayError(err)); + function sendAll(sendCb) { + var sent = 0; + var sendOnce = function () { + channel.publish('event0', messageText); + if (++sent == iterations) { + sendCb(null); + return; + } + setTimeout(sendOnce, delay); + }; + sendOnce(); + } + function recvAll(recvCb) { + var received = 0; + channel.subscribe('event0', function (msg) { + expect(msg.data == messageText).to.be.ok; + if (++received == iterations) recvCb(null); + }); + } + + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - test.ok('Verify all messages received'); - closeAndFinish(test, realtime); + async.parallel([sendAll, recvAll], function (err) { + closeAndFinish(done, realtime, err); + }); }); }); + } + + it('multiple_send_binary_2_200', function (done) { + _multiple_send(done, false, 2, 200); }); - } - exports.multiple_send_binary_2_200 = function(test) { _multiple_send(test, false, 2, 200); }; - exports.multiple_send_text_2_200 = function(test) { _multiple_send(test, true, 2, 200); }; - exports.multiple_send_binary_20_100 = function(test) { _multiple_send(test, false, 20, 100); }; - exports.multiple_send_text_20_100 = function(test) { _multiple_send(test, true, 20, 100); }; + it('multiple_send_text_2_200', function (done) { + _multiple_send(done, true, 2, 200); + }); - function _single_send_separate_realtimes(test, txOpts, rxOpts) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } + it('multiple_send_binary_20_100', function (done) { + _multiple_send(done, false, 20, 100); + }); + + it('multiple_send_text_20_100', function (done) { + _multiple_send(done, true, 20, 100); + }); - var txRealtime = helper.AblyRealtime(txOpts), - rxRealtime = helper.AblyRealtime(rxOpts), - channelName = 'single_send_separate_realtimes'; - test.expect(3); - var messageText = 'Test message for single_send_separate_realtimes', - txChannel = txRealtime.channels.get(channelName), - rxChannel = rxRealtime.channels.get(channelName); - - Crypto.generateRandomKey(function(err, key) { - if(err) { - test.ok(false, 'Unable to generate key; err = ' + displayError(err)); - closeAndFinish(test, realtime); + function _single_send_separate_realtimes(done, txOpts, rxOpts) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - async.parallel([ - function(cb) { txChannel.setOptions({cipher: {key: key}}); cb(); }, - function(cb) { rxChannel.setOptions({cipher: {key: key}}); cb(); } - ], function(err) { - if(err) { - test.ok(false, 'Unable to set cipher; err = ' + displayError(err)); - closeAndFinish(test, realtime); + + var txRealtime = helper.AblyRealtime(txOpts), + rxRealtime = helper.AblyRealtime(rxOpts), + channelName = 'single_send_separate_realtimes'; + var messageText = 'Test message for single_send_separate_realtimes', + txChannel = txRealtime.channels.get(channelName), + rxChannel = rxRealtime.channels.get(channelName); + + Crypto.generateRandomKey(function (err, key) { + if (err) { + closeAndFinish(done, realtime, err); return; } - test.equal(txChannel.channelOptions.cipher.algorithm, 'aes'); - test.equal(rxChannel.channelOptions.cipher.algorithm, 'aes'); - - attachChannels([txChannel, rxChannel], function() { - rxChannel.subscribe('event0', function(msg) { - test.ok(msg.data == messageText); - closeAndFinish(test, [txRealtime, rxRealtime]); - }); - txChannel.publish('event0', messageText); - }); + async.parallel( + [ + function (cb) { + txChannel.setOptions({ cipher: { key: key } }); + cb(); + }, + function (cb) { + rxChannel.setOptions({ cipher: { key: key } }); + cb(); + } + ], + function (err) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + try { + expect(txChannel.channelOptions.cipher.algorithm).to.equal('aes'); + expect(rxChannel.channelOptions.cipher.algorithm).to.equal('aes'); + } catch (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + } + + attachChannels([txChannel, rxChannel], function () { + rxChannel.subscribe('event0', function (msg) { + try { + expect(msg.data == messageText).to.be.ok; + } catch (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + closeAndFinish(done, [txRealtime, rxRealtime]); + }); + txChannel.publish('event0', messageText); + }); + } + ); }); - }); - } - - /** - * Connect twice to the service, using the binary protocol - * and the text protocol. Publish an encrypted message on that channel using - * the default cipher params and verify correct receipt. - */ - exports.single_send_binary_text = function(test) { - _single_send_separate_realtimes(test, { useBinaryProtocol: true }, { useBinaryProtocol: false }); - }; - - /** - * Connect twice to the service, using the text protocol and the - * binary protocol. Publish an encrypted message on that channel using - * the default cipher params and verify correct receipt. - */ - exports.single_send_text_binary = function(test) { - _single_send_separate_realtimes(test, { useBinaryProtocol: false }, { useBinaryProtocol: true }); - }; - - exports.publish_immediately = function(test) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; } - var txRealtime = helper.AblyRealtime(), - rxRealtime = helper.AblyRealtime(), - channelName = 'publish_immediately', - messageText = 'Test message'; - test.expect(1); + /** + * Connect twice to the service, using the binary protocol + * and the text protocol. Publish an encrypted message on that channel using + * the default cipher params and verify correct receipt. + */ + it('single_send_binary_text', function (done) { + _single_send_separate_realtimes(done, { useBinaryProtocol: true }, { useBinaryProtocol: false }); + }); - Crypto.generateRandomKey(function(err, key) { - if(err) { - test.ok(false, 'Unable to generate key; err = ' + displayError(err)); - closeAndFinish(test, realtime); + /** + * Connect twice to the service, using the text protocol and the + * binary protocol. Publish an encrypted message on that channel using + * the default cipher params and verify correct receipt. + */ + it('single_send_text_binary', function (done) { + _single_send_separate_realtimes(done, { useBinaryProtocol: false }, { useBinaryProtocol: true }); + }); + + it('publish_immediately', function (done) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - var rxChannel = rxRealtime.channels.get(channelName, {cipher: {key: key}}); - rxChannel.subscribe('event0', function(msg) { - test.ok(msg.data == messageText); - closeAndFinish(test, [txRealtime, rxRealtime]); - }, function() { - var txChannel = txRealtime.channels.get(channelName, {cipher: {key: key}}); - txChannel.publish('event0', messageText); - }) - }); - } - /** - * Connect twice to the service, using different cipher keys. - * Publish an encrypted message on that channel using - * the default cipher params and verify that the decrypt failure - * is noticed as bad recovered plaintext. - */ - exports.single_send_key_mismatch = function(test) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } + var txRealtime = helper.AblyRealtime(), + rxRealtime = helper.AblyRealtime(), + channelName = 'publish_immediately', + messageText = 'Test message'; - var txRealtime = helper.AblyRealtime(); - var rxRealtime = helper.AblyRealtime(); - test.expect(1); - var channelName = 'single_send_key_mismatch', - txChannel = txRealtime.channels.get(channelName), - messageText = 'Test message (' + channelName + ')', - rxChannel = rxRealtime.channels.get(channelName); - - async.parallel([ - Crypto.generateRandomKey, - Crypto.generateRandomKey, - function(cb) { attachChannels([txChannel, rxChannel], cb); } - ], function(err, res) { - if(err) { - test.ok(false, 'Unable to get cipher params; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); + Crypto.generateRandomKey(function (err, key) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + var rxChannel = rxRealtime.channels.get(channelName, { cipher: { key: key } }); + rxChannel.subscribe( + 'event0', + function (msg) { + try { + expect(msg.data == messageText).to.be.ok; + } catch (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + closeAndFinish(done, [txRealtime, rxRealtime]); + }, + function () { + var txChannel = txRealtime.channels.get(channelName, { cipher: { key: key } }); + txChannel.publish('event0', messageText); + } + ); + }); + }); + + /** + * Connect twice to the service, using different cipher keys. + * Publish an encrypted message on that channel using + * the default cipher params and verify that the decrypt failure + * is noticed as bad recovered plaintext. + */ + it('single_send_key_mismatch', function (done) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - var txKey = res[0], - rxKey = res[1]; - - async.parallel([ - function(cb) { txChannel.setOptions({cipher: {key: txKey}}); cb(); }, - function(cb) { rxChannel.setOptions({cipher: {key: rxKey}}); cb(); } - ], function() { - rxChannel.subscribe('event0', function(msg) { - test.ok(msg.data != messageText); - closeAndFinish(test, [txRealtime, rxRealtime]); - }); - txChannel.publish('event0', messageText); - }); + + var txRealtime = helper.AblyRealtime(); + var rxRealtime = helper.AblyRealtime(); + var channelName = 'single_send_key_mismatch', + txChannel = txRealtime.channels.get(channelName), + messageText = 'Test message (' + channelName + ')', + rxChannel = rxRealtime.channels.get(channelName); + + async.parallel( + [ + Crypto.generateRandomKey, + Crypto.generateRandomKey, + function (cb) { + attachChannels([txChannel, rxChannel], cb); + } + ], + function (err, res) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + var txKey = res[0], + rxKey = res[1]; + + async.parallel( + [ + function (cb) { + txChannel.setOptions({ cipher: { key: txKey } }); + cb(); + }, + function (cb) { + rxChannel.setOptions({ cipher: { key: rxKey } }); + cb(); + } + ], + function () { + rxChannel.subscribe('event0', function (msg) { + try { + expect(msg.data != messageText).to.be.ok; + } catch (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + closeAndFinish(done, [txRealtime, rxRealtime]); + }); + txChannel.publish('event0', messageText); + } + ); + } + ); }); - }; - - /** - * Connect twice to the service, one with and one without encryption. - * Publish an unencrypted message and verify that the receiving connection - * does not attempt to decrypt it. - */ - exports.single_send_unencrypted = function(test) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } - var txRealtime = helper.AblyRealtime(); - var rxRealtime = helper.AblyRealtime(); - test.expect(1); - var channelName = 'single_send_unencrypted', - txChannel = txRealtime.channels.get(channelName), - messageText = 'Test message (' + channelName + ')', - rxChannel = rxRealtime.channels.get(channelName); - - attachChannels([txChannel, rxChannel], function(err) { - if(err) { - test.ok(false, 'Unable to get attach channels; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); + /** + * Connect twice to the service, one with and one without encryption. + * Publish an unencrypted message and verify that the receiving connection + * does not attempt to decrypt it. + */ + it('single_send_unencrypted', function (done) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - Crypto.generateRandomKey(function(err, rxKey) { - if(err) { - test.ok(false, 'Unable to generate key; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); + + var txRealtime = helper.AblyRealtime(); + var rxRealtime = helper.AblyRealtime(); + var channelName = 'single_send_unencrypted', + txChannel = txRealtime.channels.get(channelName), + messageText = 'Test message (' + channelName + ')', + rxChannel = rxRealtime.channels.get(channelName); + + attachChannels([txChannel, rxChannel], function (err) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); return; } - rxChannel.setOptions({cipher: {key: rxKey}}); - rxChannel.subscribe('event0', function(msg) { - test.ok(msg.data == messageText); - closeAndFinish(test, [txRealtime, rxRealtime]); + Crypto.generateRandomKey(function (err, rxKey) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + rxChannel.setOptions({ cipher: { key: rxKey } }); + rxChannel.subscribe('event0', function (msg) { + try { + expect(msg.data == messageText).to.be.ok; + } catch (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + closeAndFinish(done, [txRealtime, rxRealtime]); + }); + txChannel.publish('event0', messageText); }); - txChannel.publish('event0', messageText); }); }); - }; - - /** - * Connect twice to the service, one with and one without encryption. - * Publish an encrypted message and verify that the receiving connection - * handles the encrypted message correctly. - */ - exports.single_send_encrypted_unhandled = function(test) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } - var txRealtime = helper.AblyRealtime(); - var rxRealtime = helper.AblyRealtime(); - test.expect(1); - var channelName = 'single_send_encrypted_unhandled', - txChannel = txRealtime.channels.get(channelName), - messageText = 'Test message (' + channelName + ')', - rxChannel = rxRealtime.channels.get(channelName); - - attachChannels([txChannel, rxChannel], function(err) { - if(err) { - test.ok(false, 'Unable to get attach channels; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); + /** + * Connect twice to the service, one with and one without encryption. + * Publish an encrypted message and verify that the receiving connection + * handles the encrypted message correctly. + */ + it('single_send_encrypted_unhandled', function (done) { + if (!Crypto) { + done(new Error('Encryption not supported')); return; } - Crypto.generateRandomKey(function(err, txKey) { - if(err) { - test.ok(false, 'Unable to generate key; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); + + var txRealtime = helper.AblyRealtime(); + var rxRealtime = helper.AblyRealtime(); + var channelName = 'single_send_encrypted_unhandled', + txChannel = txRealtime.channels.get(channelName), + messageText = 'Test message (' + channelName + ')', + rxChannel = rxRealtime.channels.get(channelName); + + attachChannels([txChannel, rxChannel], function (err) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); return; } - txChannel.setOptions({cipher: {key: txKey}}); - rxChannel.subscribe('event0', function(msg) { - test.ok(msg.encoding.indexOf('cipher') > -1); - closeAndFinish(test, [txRealtime, rxRealtime]); + Crypto.generateRandomKey(function (err, txKey) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + txChannel.setOptions({ cipher: { key: txKey } }); + rxChannel.subscribe('event0', function (msg) { + try { + expect(msg.encoding.indexOf('cipher') > -1).to.be.ok; + } catch (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + closeAndFinish(done, [txRealtime, rxRealtime]); + }); + txChannel.publish('event0', messageText); }); - txChannel.publish('event0', messageText); }); }); - }; - - /** - * Check Channel.setOptions updates CipherParams correctly: - * - publish a message using a key, verifying correct receipt; - * - publish with an updated key on the tx connection and verify that it is not decrypted by the rx connection; - * - publish with an updated key on the rx connection and verify connect receipt - */ - exports.set_cipher_params0 = function(test) { - if(!Crypto) { - test.ok(false, 'Encryption not supported'); - test.done(); - return; - } - var txRealtime = helper.AblyRealtime(); - var rxRealtime = helper.AblyRealtime(); - test.expect(3); - var channelName = 'set_cipher_params', - txChannel = txRealtime.channels.get(channelName), - messageText = 'Test message (' + channelName + ')', - rxChannel = rxRealtime.channels.get(channelName), - firstKey, secondKey; - - var waitAttach = function(cb) { attachChannels([txChannel, rxChannel], cb); }; - var setInitialOptions = function(cb) { - Crypto.generateRandomKey(function(err, key) { - if(err) { - test.ok(false, 'Unable to get cipher params; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); - return; - } - firstKey = key; - async.parallel([ - function(innercb) {rxChannel.setOptions({cipher: {key: key}}); innercb();}, - function(innercb) {txChannel.setOptions({cipher: {key: key}}); innercb();} - ], cb) - }); - }; + /** + * Check Channel.setOptions updates CipherParams correctly: + * - publish a message using a key, verifying correct receipt; + * - publish with an updated key on the tx connection and verify that it is not decrypted by the rx connection; + * - publish with an updated key on the rx connection and verify connect receipt + */ + it('set_cipher_params0', function (done) { + if (!Crypto) { + done(new Error('Encryption not supported')); + return; + } - var sendFirstMessage = function(cb) { - var handler = function(msg) { - test.ok(msg.data == messageText, 'Message data not expected value'); - rxChannel.unsubscribe('event0', handler); - cb(null); + var txRealtime = helper.AblyRealtime(); + var rxRealtime = helper.AblyRealtime(); + var channelName = 'set_cipher_params', + txChannel = txRealtime.channels.get(channelName), + messageText = 'Test message (' + channelName + ')', + rxChannel = rxRealtime.channels.get(channelName), + firstKey, + secondKey; + + var waitAttach = function (cb) { + attachChannels([txChannel, rxChannel], cb); + }; + var setInitialOptions = function (cb) { + Crypto.generateRandomKey(function (err, key) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + firstKey = key; + async.parallel( + [ + function (innercb) { + rxChannel.setOptions({ cipher: { key: key } }); + innercb(); + }, + function (innercb) { + txChannel.setOptions({ cipher: { key: key } }); + innercb(); + } + ], + cb + ); + }); }; - rxChannel.subscribe('event0', handler); - txChannel.publish('event0', messageText); - }; - - var createSecondKey = function(cb) { - Crypto.generateRandomKey(function(err, key) { - if(err) { - test.ok(false, 'Unable to get cipher params; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); - return; - } - secondKey = key; - async.parallel([ - function(innercb) {rxChannel.setOptions({cipher: null}); innercb();}, - function(innercb) {txChannel.setOptions({cipher: {key: key}}); innercb();} - ], cb) - }); - }; - var sendSecondMessage = function(cb) { - var handler = function(msg) { - test.ok(msg.encoding.indexOf('cipher') > -1, 'Message does not have cipher transform'); - rxChannel.unsubscribe('event1', handler); - cb(null); + var sendFirstMessage = function (cb) { + var handler = function (msg) { + expect(msg.data == messageText, 'Message data not expected value').to.be.ok; + rxChannel.unsubscribe('event0', handler); + cb(null); + }; + rxChannel.subscribe('event0', handler); + txChannel.publish('event0', messageText); }; - rxChannel.subscribe('event1', handler); - txChannel.publish('event1', messageText); - }; - - var setSecondKey = function(cb) { - rxChannel.setOptions({cipher: {key: secondKey}}); - cb(); - }; - - var sendThirdMessage = function(cb) { - var handler = function(msg) { - test.ok(msg.data == messageText, 'Message data not expected (third message)'); - rxChannel.unsubscribe('event2', handler); - cb(null); + + var createSecondKey = function (cb) { + Crypto.generateRandomKey(function (err, key) { + if (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + return; + } + secondKey = key; + async.parallel( + [ + function (innercb) { + rxChannel.setOptions({ cipher: null }); + innercb(); + }, + function (innercb) { + txChannel.setOptions({ cipher: { key: key } }); + innercb(); + } + ], + cb + ); + }); }; - rxChannel.subscribe('event2', handler); - txChannel.publish('event2', messageText); - }; - - async.series([ - waitAttach, - setInitialOptions, - sendFirstMessage, - createSecondKey, - sendSecondMessage, - setSecondKey, - sendThirdMessage - ], function(err) { - if(err) { - test.ok(false, 'Unexpected error running test; err = ' + displayError(err)); - closeAndFinish(test, [txRealtime, rxRealtime]); - return; - } - closeAndFinish(test, [txRealtime, rxRealtime]); + + var sendSecondMessage = function (cb) { + var handler = function (msg) { + expect(msg.encoding.indexOf('cipher') > -1, 'Message does not have cipher transform').to.be.ok; + rxChannel.unsubscribe('event1', handler); + cb(null); + }; + rxChannel.subscribe('event1', handler); + txChannel.publish('event1', messageText); + }; + + var setSecondKey = function (cb) { + rxChannel.setOptions({ cipher: { key: secondKey } }); + cb(); + }; + + var sendThirdMessage = function (cb) { + var handler = function (msg) { + expect(msg.data == messageText, 'Message data not expected (third message)').to.be.ok; + rxChannel.unsubscribe('event2', handler); + cb(null); + }; + rxChannel.subscribe('event2', handler); + txChannel.publish('event2', messageText); + }; + + async.series( + [ + waitAttach, + setInitialOptions, + sendFirstMessage, + createSecondKey, + sendSecondMessage, + setSecondKey, + sendThirdMessage + ], + function (err) { + closeAndFinish(done, [txRealtime, rxRealtime], err); + } + ); }); - }; + }); +}); - helper.withMocha('realtime/crypto', exports); -}); \ No newline at end of file diff --git a/spec/realtime/delta.test.js b/spec/realtime/delta.test.js index a6759731e0..2d4d08040f 100644 --- a/spec/realtime/delta.test.js +++ b/spec/realtime/delta.test.js @@ -1,18 +1,22 @@ -"use strict"; - -define(['shared_helper', 'vcdiff-decoder', 'async'], function(helper, vcdiffDecoder, async) { - var exports = {}, - _exports = {}, - displayError = helper.displayError, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection, - testData = [ - { foo: 'bar', count: 1, status: 'active' }, - { foo: 'bar', count: 2, status: 'active' }, - { foo: 'bar', count: 2, status: 'inactive' }, - { foo: 'bar', count: 3, status: 'inactive' }, - { foo: 'bar', count: 3, status: 'active' } - ]; +'use strict'; + +define(['shared_helper', 'vcdiff-decoder', 'async', 'chai'], function ( + helper, + vcdiffDecoder, + async, + chai +) { + var expect = chai.expect; + var displayError = helper.displayError; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; + var testData = [ + { foo: 'bar', count: 1, status: 'active' }, + { foo: 'bar', count: 2, status: 'active' }, + { foo: 'bar', count: 2, status: 'inactive' }, + { foo: 'bar', count: 3, status: 'inactive' }, + { foo: 'bar', count: 3, status: 'active' } + ]; function equals(a, b) { return JSON.stringify(a) === JSON.stringify(b); @@ -21,230 +25,257 @@ define(['shared_helper', 'vcdiff-decoder', 'async'], function(helper, vcdiffDeco function getTestVcdiffDecoder() { return { numberOfCalls: 0, - decode: function(delta, base) { + decode: function (delta, base) { this.numberOfCalls++; return vcdiffDecoder.decode(delta, base); } }; } - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, displayError(err)); - } else { - test.ok(true, 'setup app'); - } - test.done(); - }); - }; - - exports.deltaPlugin = function(test) { - test.expect(testData.length + 1); - var testName = 'deltaPlugin'; - try { - var testVcdiffDecoder = getTestVcdiffDecoder(); - var realtime = helper.AblyRealtime({ - plugins: { - vcdiff: testVcdiffDecoder - } - }); - var channel = realtime.channels.get(testName, { params: { delta: 'vcdiff' } }); + describe('realtime/delta', function () { + this.timeout(60 * 1000); - channel.attach(function(err) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); } + done(); + }); + }); - channel.on('attaching', function(stateChange) { - test.ok(false, 'Channel reattaching, presumably due to decode failure; reason =' + displayError(stateChange.reason)); + it('deltaPlugin', function (done) { + var testName = 'deltaPlugin'; + try { + var testVcdiffDecoder = getTestVcdiffDecoder(); + var realtime = helper.AblyRealtime({ + plugins: { + vcdiff: testVcdiffDecoder + } }); + var channel = realtime.channels.get(testName, { params: { delta: 'vcdiff' } }); - channel.subscribe(function(message) { - var index = Number(message.name); - test.ok(equals(testData[index], message.data), 'Check message.data'); - - if (index === testData.length - 1) { - test.equal(testVcdiffDecoder.numberOfCalls, testData.length - 1, 'Check number of delta messages'); - closeAndFinish(test, realtime); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); } - }); - async.timesSeries(testData.length, function(i, cb) { - channel.publish(i.toString(), testData[i], cb); + channel.on('attaching', function (stateChange) { + done( + new Error( + 'Channel reattaching, presumably due to decode failure; reason =' + displayError(stateChange.reason) + ) + ); + }); + + channel.subscribe(function (message) { + try { + var index = Number(message.name); + expect(equals(testData[index], message.data), 'Check message.data').to.be.ok; + + if (index === testData.length - 1) { + expect(testVcdiffDecoder.numberOfCalls).to.equal(testData.length - 1, 'Check number of delta messages'); + closeAndFinish(done, realtime); + } + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + async.timesSeries(testData.length, function (i, cb) { + channel.publish(i.toString(), testData[i], cb); + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - exports.unusedPlugin = function(test) { - test.expect(testData.length + 1); - var testName = 'unusedPlugin'; - try { - var testVcdiffDecoder = getTestVcdiffDecoder(); - var realtime = helper.AblyRealtime({ - plugins: { - vcdiff: testVcdiffDecoder - } - }); - var channel = realtime.channels.get(testName); - - channel.attach(function(err) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - } - channel.subscribe(function(message) { - var index = Number(message.name); - test.ok(equals(testData[index], message.data), 'Check message.data'); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - if (index === testData.length - 1) { - test.equal(testVcdiffDecoder.numberOfCalls, 0, 'Check number of delta messages'); - closeAndFinish(test, realtime); + it('unusedPlugin', function (done) { + var testName = 'unusedPlugin'; + try { + var testVcdiffDecoder = getTestVcdiffDecoder(); + var realtime = helper.AblyRealtime({ + plugins: { + vcdiff: testVcdiffDecoder } }); + var channel = realtime.channels.get(testName); - async.timesSeries(testData.length, function(i, cb) { - channel.publish(i.toString(), testData[i], cb); + channel.attach(function (err) { + if (err) { + closeAndFinish(doner, realtime, err); + } + channel.subscribe(function (message) { + try { + var index = Number(message.name); + expect(equals(testData[index], message.data), 'Check message.data').to.be.ok; + + if (index === testData.length - 1) { + expect(testVcdiffDecoder.numberOfCalls).to.equal(0, 'Check number of delta messages'); + closeAndFinish(done, realtime); + } + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + async.timesSeries(testData.length, function (i, cb) { + channel.publish(i.toString(), testData[i], cb); + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - exports.lastMessageNotFoundRecovery = function(test) { - test.expect(testData.length + 2); - var testName = 'lastMessageNotFoundRecovery'; - try { - var testVcdiffDecoder = getTestVcdiffDecoder(); - var realtime = helper.AblyRealtime({ - plugins: { - vcdiff: testVcdiffDecoder - } - }); - var channel = realtime.channels.get(testName, { params: { delta: 'vcdiff' } }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - channel.attach(function(err) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - } - channel.subscribe(function(message) { - var index = Number(message.name); - test.ok(equals(testData[index], message.data), 'Check message.data'); - - if (index === 1) { - /* Simulate issue */ - channel._lastPayload.messageId = null; - channel.once('attaching', function(stateChange) { - test.equal(stateChange.reason.code, 40018, 'Check error code passed through per RTL18c'); - channel.on('attaching', function(stateChange) { - test.ok(false, 'Check no further decode failures; reason =' + displayError(stateChange.reason)); - }); - }) - } else if (index === testData.length - 1) { - test.equal(testVcdiffDecoder.numberOfCalls, testData.length - 2, 'Check number of delta messages'); - closeAndFinish(test, realtime); + it('lastMessageNotFoundRecovery', function (done) { + var testName = 'lastMessageNotFoundRecovery'; + try { + var testVcdiffDecoder = getTestVcdiffDecoder(); + var realtime = helper.AblyRealtime({ + plugins: { + vcdiff: testVcdiffDecoder } }); + var channel = realtime.channels.get(testName, { params: { delta: 'vcdiff' } }); - async.timesSeries(testData.length, function(i, cb) { - channel.publish(i.toString(), testData[i], cb); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + } + channel.subscribe(function (message) { + var index = Number(message.name); + try { + expect(equals(testData[index], message.data), 'Check message.data').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + } + + if (index === 1) { + /* Simulate issue */ + channel._lastPayload.messageId = null; + channel.once('attaching', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(40018, 'Check error code passed through per RTL18c'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + channel.on('attaching', function (stateChange) { + closeAndFinish( + done, + realtime, + new Error('Check no further decode failures; reason =' + displayError(stateChange.reason)) + ); + }); + }); + } else if (index === testData.length - 1) { + try { + expect(testVcdiffDecoder.numberOfCalls).to.equal(testData.length - 2, 'Check number of delta messages'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } + }); + + async.timesSeries(testData.length, function (i, cb) { + channel.publish(i.toString(), testData[i], cb); + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - exports.deltaDecodeFailureRecovery = function(test) { - test.expect(testData.length * 2 - 1); - var testName = 'deltaDecodeFailureRecovery'; - try { - var failingTestVcdiffDecoder = { - decode: function(delta, base) { - throw new Error('Failed to decode delta.'); - } - }; + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - var realtime = helper.AblyRealtime({ - plugins: { - vcdiff: failingTestVcdiffDecoder - } - }); - var channel = realtime.channels.get(testName, { params: { delta: 'vcdiff' } }); + it('deltaDecodeFailureRecovery', function (done) { + var testName = 'deltaDecodeFailureRecovery'; + try { + var failingTestVcdiffDecoder = { + decode: function (delta, base) { + throw new Error('Failed to decode delta.'); + } + }; - channel.attach(function(err) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - } - channel.on('attaching', function(stateChange) { - test.equal(stateChange.reason.code, 40018, 'Check error code passed through per RTL18c'); + var realtime = helper.AblyRealtime({ + plugins: { + vcdiff: failingTestVcdiffDecoder + } }); - channel.subscribe(function(message) { - var index = Number(message.name); - test.ok(equals(testData[index], message.data), 'Check message.data'); + var channel = realtime.channels.get(testName, { params: { delta: 'vcdiff' } }); - if (index === testData.length - 1) { - closeAndFinish(test, realtime); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); } + channel.on('attaching', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(40018, 'Check error code passed through per RTL18c'); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + channel.subscribe(function (message) { + var index = Number(message.name); + try { + expect(equals(testData[index], message.data), 'Check message.data').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + } + + if (index === testData.length - 1) { + closeAndFinish(done, realtime); + } + }); + + async.timesSeries(testData.length, function (i, cb) { + channel.publish(i.toString(), testData[i], cb); + }); }); - async.timesSeries(testData.length, function(i, cb) { - channel.publish(i.toString(), testData[i], cb); - }); - }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - /* Check that channel becomes failed if we get deltas when we don't have a vcdiff plugin */ - exports.noPlugin = function(test) { - var testName = 'noPlugin'; - - try { - var realtime = helper.AblyRealtime(); - var channel = realtime.channels.get('noPlugin', { params: { delta: 'vcdiff' } }); - - channel.attach(function(err) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - } - channel.once('failed', function(stateChange) { - test.equal(stateChange.reason.code, 40019, 'Check error code'); - closeAndFinish(test, realtime); - }); - async.timesSeries(testData.length, function(i, cb) { - channel.publish(i.toString(), testData[i], cb); + /* Check that channel becomes failed if we get deltas when we don't have a vcdiff plugin */ + it('noPlugin', function (done) { + try { + var realtime = helper.AblyRealtime(); + var channel = realtime.channels.get('noPlugin', { params: { delta: 'vcdiff' } }); + + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + } + channel.once('failed', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(40019, 'Check error code'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + async.timesSeries(testData.length, function (i, cb) { + channel.publish(i.toString(), testData[i], cb); + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + }); +}); - helper.withMocha('realtime/delta', exports); -}); \ No newline at end of file diff --git a/spec/realtime/encoding.test.js b/spec/realtime/encoding.test.js index 00f5153a87..003cb58cc2 100644 --- a/spec/realtime/encoding.test.js +++ b/spec/realtime/encoding.test.js @@ -1,164 +1,223 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - loadTestData = helper.loadTestData, - BufferUtils = Ably.Realtime.BufferUtils, - displayError = helper.displayError, - encodingFixturesPath = helper.testResourcesPath + 'messages-encoding.json', - utils = helper.Utils, - closeAndFinish = helper.closeAndFinish; +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + var loadTestData = helper.loadTestData; + var BufferUtils = Ably.Realtime.BufferUtils; + var displayError = helper.displayError; + var encodingFixturesPath = helper.testResourcesPath + 'messages-encoding.json'; + var closeAndFinish = helper.closeAndFinish; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, displayError(err)); - } else { - test.ok(true, 'setup app'); - } - test.done(); + describe('realtime/encoding', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); }); - }; - /* Publish each fixture manually; subscribe with both a json and msgpack - * realtime, and check everything decodes correctly - */ - exports.message_decoding = function(test) { - loadTestData(encodingFixturesPath, function(err, testData) { - if(err) { - test.ok(false, 'Unable to get test assets; err = ' + displayError(err)); - return; - } - test.expect(testData.messages.length * 2); - var realtime = helper.AblyRealtime({useBinaryProtocol: false}), - binaryrealtime = helper.AblyRealtime({useBinaryProtocol: true}), - channelName = 'message_decoding', - channelPath = '/channels/' + channelName + '/messages', - channel = realtime.channels.get(channelName), - binarychannel = binaryrealtime.channels.get(channelName); + /* Publish each fixture manually; subscribe with both a json and msgpack + * realtime, and check everything decodes correctly + */ + it('message_decoding', function (done) { + loadTestData(encodingFixturesPath, function (err, testData) { + if (err) { + done(err); + return; + } + var realtime = helper.AblyRealtime({ useBinaryProtocol: false }), + binaryrealtime = helper.AblyRealtime({ useBinaryProtocol: true }), + channelName = 'message_decoding', + channelPath = '/channels/' + channelName + '/messages', + channel = realtime.channels.get(channelName), + binarychannel = binaryrealtime.channels.get(channelName); - async.parallel([ - function(attachCb) { channel.attach(attachCb); }, - function(attachCb) { binarychannel.attach(attachCb); } - ], function(err) { - if(err) { - test.ok(false, 'Error attaching to channel: ' + displayError(err)); - closeAndFinish(test, [realtime, binaryrealtime]); - return; - } - async.eachOf(testData.messages, function(encodingSpec, index, eachOfCb) { - /* Restricting to event name allows us to run in parallel */ - var name = index.toString(); - async.parallel([ - function(parallelCb) { - channel.subscribe(name, function(msg) { - if(encodingSpec.expectedHexValue) { - test.equal(BufferUtils.hexEncode(msg.data), encodingSpec.expectedHexValue, 'Check data matches'); - } else { - test.deepEqual(msg.data, encodingSpec.expectedValue, 'Check data matches'); - } - channel.unsubscribe(name); - parallelCb(); - }); - }, - function(parallelCb) { - binarychannel.subscribe(name, function(msg) { - if(encodingSpec.expectedHexValue) { - test.equal(BufferUtils.hexEncode(msg.data), encodingSpec.expectedHexValue, 'Check data matches'); - } else { - test.deepEqual(msg.data, encodingSpec.expectedValue, 'Check data matches'); - } - binarychannel.unsubscribe(name); - parallelCb(); - }); + async.parallel( + [ + function (attachCb) { + channel.attach(attachCb); + }, + function (attachCb) { + binarychannel.attach(attachCb); + } + ], + function (err) { + if (err) { + closeAndFinish(done, [realtime, binaryrealtime], err); + return; + } + async.eachOf( + testData.messages, + function (encodingSpec, index, eachOfCb) { + /* Restricting to event name allows us to run in parallel */ + var name = index.toString(); + async.parallel( + [ + function (parallelCb) { + channel.subscribe(name, function (msg) { + try { + if (encodingSpec.expectedHexValue) { + expect(BufferUtils.hexEncode(msg.data)).to.equal( + encodingSpec.expectedHexValue, + 'Check data matches' + ); + } else { + expect(msg.data).to.deep.equal(encodingSpec.expectedValue, 'Check data matches'); + } + channel.unsubscribe(name); + parallelCb(); + } catch (err) { + parallelCb(err); + } + }); + }, + function (parallelCb) { + binarychannel.subscribe(name, function (msg) { + try { + if (encodingSpec.expectedHexValue) { + expect(BufferUtils.hexEncode(msg.data)).to.equal( + encodingSpec.expectedHexValue, + 'Check data matches' + ); + } else { + expect(msg.data).to.deep.equal(encodingSpec.expectedValue, 'Check data matches'); + } + binarychannel.unsubscribe(name); + parallelCb(); + } catch (err) { + parallelCb(err); + } + }); + }, + function (parallelCb) { + realtime.request( + 'post', + channelPath, + null, + { name: name, data: encodingSpec.data, encoding: encodingSpec.encoding }, + null, + function (err) { + parallelCb(err); + } + ); + } + ], + eachOfCb + ); }, - function(parallelCb) { - realtime.request('post', channelPath, null, {name: name, data: encodingSpec.data, encoding: encodingSpec.encoding}, null, function(err) { - parallelCb(err); - }); + function (err) { + closeAndFinish(done, [realtime, binaryrealtime], err); } - ], eachOfCb); - }, function(err) { - if(err) test.ok(false, displayError(err)); - closeAndFinish(test, [realtime, binaryrealtime]); - }); - }); + ); + } + ); + }); }); - }; - /* Publish each fixture with both a json and msgpack realtime, get history - * manually, and check everything was encoded correctly - */ - exports.message_encoding = function(test) { - loadTestData(encodingFixturesPath, function(err, testData) { - if(err) { - test.ok(false, 'Unable to get test assets; err = ' + displayError(err)); - return; - } - test.expect(testData.messages.length * 5); - var realtime = helper.AblyRealtime({useBinaryProtocol: false}), - binaryrealtime = helper.AblyRealtime({useBinaryProtocol: true}), - channelName = 'message_encoding', - channelPath = '/channels/' + channelName + '/messages', - channel = realtime.channels.get(channelName), - binarychannel = binaryrealtime.channels.get(channelName); + /* Publish each fixture with both a json and msgpack realtime, get history + * manually, and check everything was encoded correctly + */ + it('message_encoding', function (done) { + loadTestData(encodingFixturesPath, function (err, testData) { + if (err) { + done(new Error('Unable to get test assets; err = ' + displayError(err))); + return; + } + var realtime = helper.AblyRealtime({ useBinaryProtocol: false }), + binaryrealtime = helper.AblyRealtime({ useBinaryProtocol: true }), + channelName = 'message_encoding', + channelPath = '/channels/' + channelName + '/messages', + channel = realtime.channels.get(channelName), + binarychannel = binaryrealtime.channels.get(channelName); - async.parallel([ - function(attachCb) { channel.attach(attachCb); }, - function(attachCb) { binarychannel.attach(attachCb); } - ], function(err) { - if(err) { - test.ok(false, 'Error attaching to channel: ' + displayError(err)); - closeAndFinish(test, [realtime, binaryrealtime]); - return; - } - async.eachOf(testData.messages, function(encodingSpec, index, eachOfCb) { - /* Restricting to event name allows us to run in parallel */ - var data, name = index.toString(); - if(encodingSpec.expectedHexValue) { - data = BufferUtils.base64Decode(encodingSpec.data); - } else { - data = encodingSpec.expectedValue; + async.parallel( + [ + function (attachCb) { + channel.attach(attachCb); + }, + function (attachCb) { + binarychannel.attach(attachCb); } - async.parallel([ - function(parallelCb) { - channel.publish(name, data, parallelCb); - }, - function(parallelCb) { - binarychannel.publish(name, data, parallelCb); - } - ], function(err) { - if(err) { - eachOfCb(err); - return; - } - realtime.request('get', channelPath, null, null, null, function(err, resultPage) { - if(err) { - eachOfCb(err); - return; - } - var msgs = helper.arrFilter(resultPage.items, function(m) {return m.name === name;}); - test.equal(msgs.length, 2, 'Check expected number of results (one from json rt, one from binary rt)'); - test.ok(msgs[0].encoding == encodingSpec.encoding, 'Check encodings match'); - test.ok(msgs[1].encoding == encodingSpec.encoding, 'Check encodings match'); - if(msgs[0].encoding === 'json') { - test.deepEqual(JSON.parse(encodingSpec.data), JSON.parse(msgs[0].data), 'Check data matches'); - test.deepEqual(JSON.parse(encodingSpec.data), JSON.parse(msgs[1].data), 'Check data matches'); + ], + function (err) { + if (err) { + closeAndFinish(done, [realtime, binaryrealtime], err); + return; + } + async.eachOf( + testData.messages, + function (encodingSpec, index, eachOfCb) { + /* Restricting to event name allows us to run in parallel */ + var data, + name = index.toString(); + if (encodingSpec.expectedHexValue) { + data = BufferUtils.base64Decode(encodingSpec.data); } else { - test.equal(encodingSpec.data, msgs[0].data, 'Check data matches'); - test.equal(encodingSpec.data, msgs[1].data, 'Check data matches'); + data = encodingSpec.expectedValue; } - eachOfCb(); - }); - }); - }, function(err) { - if(err) test.ok(false, displayError(err)); - closeAndFinish(test, [realtime, binaryrealtime]); - }); - }); + async.parallel( + [ + function (parallelCb) { + channel.publish(name, data, parallelCb); + }, + function (parallelCb) { + binarychannel.publish(name, data, parallelCb); + } + ], + function (err) { + if (err) { + eachOfCb(err); + return; + } + realtime.request('get', channelPath, null, null, null, function (err, resultPage) { + if (err) { + eachOfCb(err); + return; + } + try { + var msgs = helper.arrFilter(resultPage.items, function (m) { + return m.name === name; + }); + expect(msgs.length).to.equal( + 2, + 'Check expected number of results (one from json rt, one from binary rt)' + ); + expect(msgs[0].encoding == encodingSpec.encoding, 'Check encodings match').to.be.ok; + expect(msgs[1].encoding == encodingSpec.encoding, 'Check encodings match').to.be.ok; + if (msgs[0].encoding === 'json') { + expect(JSON.parse(encodingSpec.data)).to.deep.equal( + JSON.parse(msgs[0].data), + 'Check data matches' + ); + expect(JSON.parse(encodingSpec.data)).to.deep.equal( + JSON.parse(msgs[1].data), + 'Check data matches' + ); + } else { + expect(encodingSpec.data).to.equal(msgs[0].data, 'Check data matches'); + expect(encodingSpec.data).to.equal(msgs[1].data, 'Check data matches'); + } + eachOfCb(); + } catch (err) { + eachOfCb(err); + } + }); + } + ); + }, + function (err) { + closeAndFinish(done, [realtime, binaryrealtime], err); + } + ); + } + ); + }); }); - }; + }); +}); - helper.withMocha('realtime/encoding', exports); -}); \ No newline at end of file diff --git a/spec/realtime/event_emitter.test.js b/spec/realtime/event_emitter.test.js index 36f85afcf2..b0fee9ecaa 100644 --- a/spec/realtime/event_emitter.test.js +++ b/spec/realtime/event_emitter.test.js @@ -1,356 +1,418 @@ -"use strict"; - -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}, - displayError = helper.displayError, - utils = helper.Utils, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); - }; - - /* - * Check all eight events associated with connecting, attaching to a - * channel, detaching, and disconnecting are received once each - */ - exports.attachdetach0 = function(test) { - test.expect(8); - try { - /* Note: realtime now sends an ATTACHED post-upgrade, which can race with - * the DETACHED if the DETACH is only sent just after upgrade. Remove - * bestTransport with 1.1 spec which has IDs in ATTACHs */ - var realtime = helper.AblyRealtime({transports: [helper.bestTransport]}), - index, - expectedConnectionEvents = [ - 'connecting', - 'connected', - 'closing', - 'closed' - ], - expectedChannelEvents= [ - 'attaching', - 'attached', - 'detaching', - 'detached' - ]; - realtime.connection.on(function() { - if((index = utils.arrIndexOf(expectedConnectionEvents, this.event)) > -1) { - delete expectedConnectionEvents[index]; - test.ok(true, this.event + ' connection event received'); - if(this.event == 'closed') { - test.done(); - } - } else { - test.ok(false, 'Unexpected ' + this.event + ' event received'); +'use strict'; + +define(['shared_helper', 'chai'], function (helper, chai) { + var expect = chai.expect; + var displayError = helper.displayError; + var utils = helper.Utils; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; + + describe('realtime/event_emitter', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; } + done(); }); - realtime.connection.on('connected', function() { - var channel = realtime.channels.get('channel'); - channel.on(function() { - if((index = utils.arrIndexOf(expectedChannelEvents, this.event)) > -1) { - delete expectedChannelEvents[index]; - test.ok(true, this.event + ' channel event received'); - switch(this.event) { - case 'detached': - realtime.close(); - break; - case 'attached': - channel.detach(); - break; - default: - break; + }); + + /* + * Check all eight events associated with connecting, attaching to a + * channel, detaching, and disconnecting are received once each + */ + it('attachdetach0', function (done) { + try { + /* Note: realtime now sends an ATTACHED post-upgrade, which can race with + * the DETACHED if the DETACH is only sent just after upgrade. Remove + * bestTransport with 1.1 spec which has IDs in ATTACHs */ + var realtime = helper.AblyRealtime({ transports: [helper.bestTransport] }), + index, + expectedConnectionEvents = ['connecting', 'connected', 'closing', 'closed'], + expectedChannelEvents = ['attaching', 'attached', 'detaching', 'detached']; + realtime.connection.on(function () { + if ((index = utils.arrIndexOf(expectedConnectionEvents, this.event)) > -1) { + delete expectedConnectionEvents[index]; + if (this.event == 'closed') { + done(); } } else { - test.ok(false, 'Unexpected ' + this.event + ' event received'); + done(new Error('Unexpected ' + this.event + ' event received')); } }); - channel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - } + realtime.connection.on('connected', function () { + var channel = realtime.channels.get('channel'); + channel.on(function () { + if ((index = utils.arrIndexOf(expectedChannelEvents, this.event)) > -1) { + delete expectedChannelEvents[index]; + switch (this.event) { + case 'detached': + realtime.close(); + break; + case 'attached': + channel.detach(); + break; + default: + break; + } + } else { + done(new Error('Unexpected ' + this.event + ' event received')); + } + }); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + } + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - exports.emitCallsAllCallbacksIgnoringExceptions = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + it('emitCallsAllCallbacksIgnoringExceptions', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = false, eventEmitter = realtime.connection; - eventEmitter.on('custom', function() { throw('Expected failure 1'); }); - eventEmitter.on('custom', function() { throw('Expected failure 2'); }); - eventEmitter.on('custom', function() { callbackCalled = true; }); + eventEmitter.on('custom', function () { + throw 'Expected failure 1'; + }); + eventEmitter.on('custom', function () { + throw 'Expected failure 2'; + }); + eventEmitter.on('custom', function () { + callbackCalled = true; + }); - eventEmitter.emit('custom'); - test.ok(callbackCalled, 'Last callback should have been called'); - closeAndFinish(test, realtime); - } + eventEmitter.emit('custom'); + try { + expect(callbackCalled, 'Last callback should have been called').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); - exports.onceCalledOnlyOnce = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('onceCalledOnlyOnce', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), onCallbackCalled = 0, onceCallbackCalled = 0, eventEmitter = realtime.connection; - eventEmitter.once('custom', function() { onceCallbackCalled += 1; }); - eventEmitter.on('custom', function() { onCallbackCalled += 1; }); + eventEmitter.once('custom', function () { + onceCallbackCalled += 1; + }); + eventEmitter.on('custom', function () { + onCallbackCalled += 1; + }); - eventEmitter.emit('custom'); - eventEmitter.emit('custom'); - eventEmitter.emit('custom'); + eventEmitter.emit('custom'); + eventEmitter.emit('custom'); + eventEmitter.emit('custom'); - test.equal(onCallbackCalled, 3, 'On callback called every time'); - test.equal(onceCallbackCalled, 1, 'Once callback called once'); + try { + expect(onCallbackCalled).to.equal(3, 'On callback called every time'); + expect(onceCallbackCalled).to.equal(1, 'Once callback called once'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - exports.onceCallbackDoesNotImpactOnCallback = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('onceCallbackDoesNotImpactOnCallback', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = 0, eventEmitter = realtime.connection; - var callback = function() { callbackCalled += 1; }; + var callback = function () { + callbackCalled += 1; + }; - eventEmitter.on('custom', callback); - eventEmitter.once('custom', callback); - eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + eventEmitter.once('custom', callback); + eventEmitter.once('custom', callback); - eventEmitter.emit('custom'); - eventEmitter.emit('custom'); + eventEmitter.emit('custom'); + eventEmitter.emit('custom'); - test.equal(callbackCalled, 4, 'On callback called both times but once callbacks only called once'); + try { + expect(callbackCalled).to.equal(4, 'On callback called both times but once callbacks only called once'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - exports.offRemovesAllMatchingListeners = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('offRemovesAllMatchingListeners', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = 0, eventEmitter = realtime.connection; - var callback = function() { callbackCalled += 1; }; - - eventEmitter.once('custom', callback); - eventEmitter.on('custom', callback); - eventEmitter.on('custom', callback); - - eventEmitter.emit('custom'); - test.equal(callbackCalled, 3, 'The same callback should have been called for every registration'); - - callbackCalled = 0; - eventEmitter.off(callback); - eventEmitter.emit('custom'); - test.equal(callbackCalled, 0, 'All callbacks should have been removed'); + var callback = function () { + callbackCalled += 1; + }; + + try { + eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + eventEmitter.on('custom', callback); + + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(3, 'The same callback should have been called for every registration'); + + callbackCalled = 0; + eventEmitter.off(callback); + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(0, 'All callbacks should have been removed'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - exports.offRemovesAllListeners = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('offRemovesAllListeners', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = 0, eventEmitter = realtime.connection; - var callback = function() { callbackCalled += 1; }; - - eventEmitter.once('custom', callback); - eventEmitter.on('custom', callback); - eventEmitter.on('custom', callback); - - eventEmitter.emit('custom'); - test.equal(callbackCalled, 3, 'The same callback should have been called for every registration'); - - callbackCalled = 0; - eventEmitter.off(); - eventEmitter.emit('custom'); - test.equal(callbackCalled, 0, 'All callbacks should have been removed'); + var callback = function () { + callbackCalled += 1; + }; + + try { + eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + eventEmitter.on('custom', callback); + + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(3, 'The same callback should have been called for every registration'); + + callbackCalled = 0; + eventEmitter.off(); + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(0, 'All callbacks should have been removed'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - exports.offRemovesAllMatchingEventListeners = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('offRemovesAllMatchingEventListeners', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = 0, eventEmitter = realtime.connection; - var callback = function() { callbackCalled += 1; }; - - eventEmitter.once('custom', callback); - eventEmitter.on('custom', callback); - eventEmitter.on('custom', callback); - - eventEmitter.emit('custom'); - test.equal(callbackCalled, 3, 'The same callback should have been called for every registration'); - - callbackCalled = 0; - eventEmitter.off('custom', callback); - eventEmitter.emit('custom'); - test.equal(callbackCalled, 0, 'All callbacks should have been removed'); + var callback = function () { + callbackCalled += 1; + }; + + try { + eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + eventEmitter.on('custom', callback); + + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(3, 'The same callback should have been called for every registration'); + + callbackCalled = 0; + eventEmitter.off('custom', callback); + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(0, 'All callbacks should have been removed'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - exports.offRemovesAllMatchingEvents = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('offRemovesAllMatchingEvents', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = 0, eventEmitter = realtime.connection; - var callback = function() { callbackCalled += 1; }; - - eventEmitter.once('custom', callback); - eventEmitter.on('custom', callback); - eventEmitter.on('custom', callback); - - eventEmitter.emit('custom'); - test.equal(callbackCalled, 3, 'The same callback should have been called for every registration'); - - callbackCalled = 0; - eventEmitter.off('custom'); - eventEmitter.emit('custom'); - test.equal(callbackCalled, 0, 'All callbacks should have been removed'); + var callback = function () { + callbackCalled += 1; + }; + + try { + eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + eventEmitter.on('custom', callback); + + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(3, 'The same callback should have been called for every registration'); + + callbackCalled = 0; + eventEmitter.off('custom'); + eventEmitter.emit('custom'); + expect(callbackCalled).to.equal(0, 'All callbacks should have been removed'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - /** - * Ensures that when a listener is removed and there - * are no more listeners for that event name, - * the key is removed entirely from listeners to avoid the - * listener object growing with unnecessary empty arrays - * for each previously registered event name - */ - exports.offRemovesEmptyEventNameListeners = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + /** + * Ensures that when a listener is removed and there + * are no more listeners for that event name, + * the key is removed entirely from listeners to avoid the + * listener object growing with unnecessary empty arrays + * for each previously registered event name + */ + it('offRemovesEmptyEventNameListeners', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), eventEmitter = realtime.connection; - var callback = function() {}; + var callback = function () {}; - eventEmitter.once('custom', callback); - eventEmitter.on('custom', callback); - test.ok('custom' in eventEmitter.events, 'custom event array exists'); + try { + eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + expect('custom' in eventEmitter.events, 'custom event array exists').to.be.ok; - eventEmitter.off('custom', callback); - test.ok(!('custom' in eventEmitter.events), 'custom event listener array is removed from object'); + eventEmitter.off('custom', callback); + expect(!('custom' in eventEmitter.events), 'custom event listener array is removed from object').to.be.ok; - eventEmitter.once('custom', callback); - eventEmitter.on('custom', callback); - test.ok('custom' in eventEmitter.events, 'custom event array exists'); + eventEmitter.once('custom', callback); + eventEmitter.on('custom', callback); + expect('custom' in eventEmitter.events, 'custom event array exists').to.be.ok; - eventEmitter.off(callback); - test.ok(!('custom' in eventEmitter.events), 'event listener array is removed from object'); + eventEmitter.off(callback); + expect(!('custom' in eventEmitter.events), 'event listener array is removed from object').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - closeAndFinish(test, realtime); - } + closeAndFinish(done, realtime); + }); - exports.arrayOfEvents = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), + it('arrayOfEvents', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), callbackCalled = 0, eventEmitter = realtime.connection; - var callback = function() { callbackCalled += 1; }; - - callbackCalled = 0; - eventEmitter.on(['a', 'b', 'c'], callback); - eventEmitter.emit('a'); - eventEmitter.emit('b'); - eventEmitter.emit('c'); - test.equal(callbackCalled, 3, 'listener listens to all events in array'); - - eventEmitter.off(['a', 'b', 'c'], callback); - eventEmitter.emit('a'); - eventEmitter.emit('b'); - eventEmitter.emit('c'); - test.equal(callbackCalled, 3, 'All callbacks should have been removed'); - - callbackCalled = 0; - eventEmitter.on(['a', 'b', 'c'], callback); - eventEmitter.off('a', callback); - eventEmitter.emit('a'); - test.equal(callbackCalled, 0, 'callback ‘a’ should have been removed'); - eventEmitter.emit('b'); - eventEmitter.emit('c'); - test.equal(callbackCalled, 2, 'callbacks b and c should not have been removed'); - - closeAndFinish(test, realtime); - } - - /* check that listeners added in a listener cb are not called during that - * emit instance */ - exports.listenerAddedInListenerCb = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), - eventEmitter = realtime.connection, - firstCbCalled = false, - secondCbCalled = false; - - eventEmitter.on('a', function() { - firstCbCalled = true; - eventEmitter.on('a', function() { - secondCbCalled = true; - }); - }); - eventEmitter.emit('a'); - - test.ok(firstCbCalled, 'check first callback called'); - test.ok(!secondCbCalled, 'check second callback not called'); - - closeAndFinish(test, realtime); - }; - - /* check that listeners removed in a listener cb are still called in that - * emit instance (but only once) */ - exports.listenerRemovedInListenerCb = function(test) { - var realtime = helper.AblyRealtime({ autoConnect: false }), - eventEmitter = realtime.connection, - onCbCalledTimes = 0, - onceCbCalledTimes = 0, - anyCbCalledTimes = 0, - anyOnceCbCalledTimes = 0; - - eventEmitter.on('a', function() { - onCbCalledTimes++; - eventEmitter.off('a'); - }); + var callback = function () { + callbackCalled += 1; + }; + + try { + callbackCalled = 0; + eventEmitter.on(['a', 'b', 'c'], callback); + eventEmitter.emit('a'); + eventEmitter.emit('b'); + eventEmitter.emit('c'); + expect(callbackCalled).to.equal(3, 'listener listens to all events in array'); + + eventEmitter.off(['a', 'b', 'c'], callback); + eventEmitter.emit('a'); + eventEmitter.emit('b'); + eventEmitter.emit('c'); + expect(callbackCalled).to.equal(3, 'All callbacks should have been removed'); + + callbackCalled = 0; + eventEmitter.on(['a', 'b', 'c'], callback); + eventEmitter.off('a', callback); + eventEmitter.emit('a'); + expect(callbackCalled).to.equal(0, 'callback ‘a’ should have been removed'); + eventEmitter.emit('b'); + eventEmitter.emit('c'); + expect(callbackCalled).to.equal(2, 'callbacks b and c should not have been removed'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - eventEmitter.once('a', function() { - onceCbCalledTimes++; - eventEmitter.off('a'); + closeAndFinish(done, realtime); }); - eventEmitter.on(function() { - anyCbCalledTimes++; - eventEmitter.off(); - }); + /* check that listeners added in a listener cb are not called during that + * emit instance */ + it('listenerAddedInListenerCb', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + eventEmitter = realtime.connection, + firstCbCalled = false, + secondCbCalled = false; + + eventEmitter.on('a', function () { + firstCbCalled = true; + eventEmitter.on('a', function () { + secondCbCalled = true; + }); + }); + eventEmitter.emit('a'); + + try { + expect(firstCbCalled, 'check first callback called').to.be.ok; + expect(!secondCbCalled, 'check second callback not called').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - eventEmitter.once(function() { - anyOnceCbCalledTimes++; - eventEmitter.off(); + closeAndFinish(done, realtime); }); - eventEmitter.emit('a'); + /* check that listeners removed in a listener cb are still called in that + * emit instance (but only once) */ + it('listenerRemovedInListenerCb', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + eventEmitter = realtime.connection, + onCbCalledTimes = 0, + onceCbCalledTimes = 0, + anyCbCalledTimes = 0, + anyOnceCbCalledTimes = 0; + + eventEmitter.on('a', function () { + onCbCalledTimes++; + eventEmitter.off('a'); + }); - test.equal(onCbCalledTimes, 1, 'check on callback called exactly once'); - test.equal(onceCbCalledTimes, 1, 'check once callback called exactly once'); - test.equal(anyCbCalledTimes, 1, 'check any callback called exactly once'); - test.equal(anyOnceCbCalledTimes, 1, 'check anyOnce callback called exactly once'); + eventEmitter.once('a', function () { + onceCbCalledTimes++; + eventEmitter.off('a'); + }); - closeAndFinish(test, realtime); - } + eventEmitter.on(function () { + anyCbCalledTimes++; + eventEmitter.off(); + }); + + eventEmitter.once(function () { + anyOnceCbCalledTimes++; + eventEmitter.off(); + }); + + eventEmitter.emit('a'); + + try { + expect(onCbCalledTimes).to.equal(1, 'check on callback called exactly once'); + expect(onceCbCalledTimes).to.equal(1, 'check once callback called exactly once'); + expect(anyCbCalledTimes).to.equal(1, 'check any callback called exactly once'); + expect(anyOnceCbCalledTimes).to.equal(1, 'check anyOnce callback called exactly once'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + closeAndFinish(done, realtime); + }); + }); +}); - helper.withMocha('realtime/event_emitter', exports); -}); \ No newline at end of file diff --git a/spec/realtime/failure.test.js b/spec/realtime/failure.test.js index c31208aee3..2dae0059a8 100644 --- a/spec/realtime/failure.test.js +++ b/spec/realtime/failure.test.js @@ -1,446 +1,543 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - _exports = {}, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection, - utils = helper.Utils, - noop = function() {}, - simulateDroppedConnection = helper.simulateDroppedConnection, - createPM = Ably.Realtime.ProtocolMessage.fromDeserialized, - availableTransports = helper.availableTransports; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var utils = helper.Utils; + var noop = function () {}; + var simulateDroppedConnection = helper.simulateDroppedConnection; + var createPM = Ably.Realtime.ProtocolMessage.fromDeserialized; + var availableTransports = helper.availableTransports; + + describe('realtime/failure', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); + }); + + /* + * Connect with invalid credentials on various transports; connection state should be 'failed' + */ + it('invalid_cred_failure', function (done) { + try { + var failure_test = function (transports) { + return function (cb) { + var realtime = helper.AblyRealtime({ key: 'this.is:wrong', transports: transports }); + realtime.connection.on('failed', function (connectionStateChange) { + try { + expect(realtime.connection.errorReason.code).to.equal(40400, 'wrong error reason code on connection.'); + expect(connectionStateChange.reason.code).to.equal( + 40400, + 'wrong error reason code on connectionStateChange' + ); + expect(connectionStateChange.reason).to.deep.equal( + realtime.connection.errorReason, + 'error reason was not equally set on connection and connectionStateChange' + ); + } catch (err) { + cb(err, realtime); + return; + } + cb(null, realtime); + }); + realtime.connection.on('disconnected', function () { + cb( + new Error('connection state for transports ' + transports + ' should be failed, not disconnected'), + realtime + ); + }); + }; + }; + async.parallel( + utils + .arrMap(availableTransports, function (transport) { + return failure_test([transport]); + }) + .concat(failure_test(null)), // to test not specifying a transport (so will use upgrade mechanism) + function (err, realtimes) { + closeAndFinish(done, realtimes, err); + } + ); + } catch (err) { + done(err); } - test.done(); }); - }; - - /* - * Connect with invalid credentials on various transports; connection state should be 'failed' - */ - exports.invalid_cred_failure = function(test) { - test.expect((availableTransports.length + 1)*4); - try { - var failure_test = function(transports) { - return function(cb) { - var realtime = helper.AblyRealtime({key: "this.is:wrong", transports: transports}); - realtime.connection.on('failed', function(connectionStateChange) { - test.ok(true, 'connection state for ' + transports + ' was failed, as expected'); - test.equal(realtime.connection.errorReason.code, 40400, 'wrong error reason code on connection.'); - test.equal(connectionStateChange.reason.code, 40400, 'wrong error reason code on connectionStateChange'); - test.deepEqual(connectionStateChange.reason, realtime.connection.errorReason, 'error reason was not equally set on connection and connectionStateChange'); - cb(null, realtime); - }); - realtime.connection.on('disconnected', function() { - test.ok(false, 'connection state for transports ' + transports + ' should be failed, not disconnected'); - cb(null, realtime); - }); + + /* + * Connect with various transports, forcibly break the transport, connection state + * should be 'disconnected' + */ + it('break_transport', function (done) { + try { + var break_test = function (transports) { + return function (cb) { + var realtime = helper.AblyRealtime({ transports: transports }); + realtime.connection.once('connected', function () { + realtime.connection.once('disconnected', function () { + cb(null, realtime); + }); + realtime.connection.on('failed', function () { + cb( + new Error('connection state for transports ' + transports + ' should be disconnected, not failed'), + realtime + ); + }); + simulateDroppedConnection(realtime); + }); + }; }; - }; - async.parallel( - utils.arrMap(availableTransports, function(transport) { - return failure_test([transport]); - }).concat(failure_test(null)), // to test not specifying a transport (so will use upgrade mechanism) - function(err, realtimes) { - if(err) { - test.ok(false, helper.displayError(err)); + async.parallel( + utils + .arrMap(availableTransports, function (transport) { + return break_test([transport]); + }) + .concat(break_test(null)), // to test not specifying a transport (so will use upgrade mechanism) + function (err, realtimes) { + closeAndFinish(done, realtimes, err); } - closeAndFinish(test, realtimes); - } - ); - } catch(e) { - test.ok(false, 'connection failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - /* - * Connect with various transports, forcibly break the transport, connection state - * should be 'disconnected' - */ - exports.break_transport = function(test) { - test.expect(availableTransports.length + 1); - try { - var break_test = function(transports) { - return function(cb) { - var realtime = helper.AblyRealtime({transports: transports}); - realtime.connection.once('connected', function() { - realtime.connection.once('disconnected', function() { - test.ok(true, 'connection state for ' + transports + ' was disconnected, as expected'); - cb(null, realtime); + ); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + /* + * Connect with various transports with a bad host, check that + * the connecting/disconnecting/suspended cycle works as expected + */ + it('no_connection_lifecycle', function (done) { + try { + var lifecycleTest = function (transports) { + return function (cb) { + var connectionEvents = []; + var realtime = helper.AblyRealtime({ + transports: transports, + realtimeHost: 'invalid', + restHost: 'invalid', + /* Timings note: some transports fail immediately with an invalid + * host, others take longer; so set the realtimeRequestTimeout to be + * small enough that the max difference is never large enough that + * the suspended timeout trips before three connection cycles */ + disconnectedRetryTimeout: 1000, + realtimeRequestTimeout: 50, + preferenceConnectTimeout: 50, + suspendedRetryTimeout: 1000, + connectionStateTtl: 2900 }); - realtime.connection.on('failed', function() { - test.ok(false, 'connection state for transports ' + transports + ' should be disconnected, not failed'); - cb(null, realtime); + realtime.connection.on(function () { + connectionEvents.push(this.event); }); - simulateDroppedConnection(realtime); - }); + + /* After 4s, has been through three connecting/disconnected cycles + * and one connecting/suspended cycles */ + var expectedConnectionEvents = [ + 'connecting', + 'disconnected', // immediately + 'connecting', + 'disconnected', // at 1s + 'connecting', + 'disconnected', // at 2s + 'suspended', // at 2.9s + 'connecting', + 'suspended' // at 3.9s + ]; + setTimeout(function () { + try { + expect(connectionEvents).to.deep.equal( + expectedConnectionEvents, + 'connection state for ' + + transports + + ' was ' + + connectionEvents + + ', expected ' + + expectedConnectionEvents + ); + realtime.close(); + cb(null, realtime); + } catch (err) { + realtime.close(); + cb(err); + } + }, 4800); + }; }; - }; - async.parallel( - utils.arrMap(availableTransports, function(transport) { - return break_test([transport]); - }).concat(break_test(null)), // to test not specifying a transport (so will use upgrade mechanism) - function(err, realtimes) { - if(err) { - test.ok(false, helper.displayError(err)); + async.parallel( + utils + .arrMap(availableTransports, function (transport) { + return lifecycleTest([transport]); + }) + .concat(lifecycleTest(null)), // to test not specifying a transport (so will use upgrade mechanism) + function (err) { + if (err) { + done(err); + } + done(); } - closeAndFinish(test, realtimes); - } - ); - } catch(e) { - test.ok(false, 'connection failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - /* - * Connect with various transports with a bad host, check that - * the connecting/disconnecting/suspended cycle works as expected - */ - exports.no_connection_lifecycle = function(test) { - test.expect(availableTransports.length + 1); - - try { - var lifecycleTest = function(transports) { - return function(cb) { - var connectionEvents = []; - var realtime = helper.AblyRealtime({ - transports: transports, - realtimeHost: 'invalid', - restHost: 'invalid', - /* Timings note: some transports fail immediately with an invalid - * host, others take longer; so set the realtimeRequestTimeout to be - * small enough that the max difference is never large enough that - * the suspended timeout trips before three connection cycles */ - disconnectedRetryTimeout: 1000, - realtimeRequestTimeout: 50, - preferenceConnectTimeout: 50, - suspendedRetryTimeout: 1000, - connectionStateTtl: 2900 + ); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + /* + * Check operations on a failed channel give the right errors + */ + it('failed_channel', function (done) { + var realtime = helper.AblyRealtime(); + var failChan; + var channelFailedCode = 90001; + + var tests = [ + function (callback) { + failChan.publish('event', 'data', function (err) { + try { + expect(err, 'publish failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'publish failure code'); + callback(); + } catch (err) { + callback(err); + } }); - realtime.connection.on(function() { - connectionEvents.push(this.event); + }, + function (callback) { + failChan.subscribe('event', noop, function (err) { + try { + expect(err, 'subscribe failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'subscribe failure code'); + callback(); + } catch (err) { + callback(err); + } }); - - /* After 4s, has been through three connecting/disconnected cycles - * and one connecting/suspended cycles */ - var expectedConnectionEvents = [ - 'connecting','disconnected', // immediately - 'connecting','disconnected', // at 1s - 'connecting','disconnected', // at 2s - 'suspended', // at 2.9s - 'connecting', 'suspended' // at 3.9s - ]; - setTimeout(function() { + }, + function (callback) { + failChan.presence.enterClient('clientId', function (err) { try { - test.deepEqual(connectionEvents, expectedConnectionEvents, 'connection state for ' + transports + ' was ' + connectionEvents + ', expected ' + expectedConnectionEvents); - realtime.close(); - cb(null, realtime); + expect(err, 'presence enter failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'presence enter failure code'); + callback(); } catch (err) { - realtime.close(); - cb(err); + callback(err); } - }, 4800); - }; - }; - async.parallel( - utils.arrMap(availableTransports, function(transport) { - return lifecycleTest([transport]); - }).concat(lifecycleTest(null)), // to test not specifying a transport (so will use upgrade mechanism) - function(err) { - if (err) { - throw err + }); + }, + function (callback) { + failChan.presence.leaveClient('clientId', function (err) { + try { + expect(err, 'presence leave failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'presence leave failure code'); + callback(); + } catch (err) { + callback(err); + } + }); + }, + function (callback) { + failChan.presence.subscribe('event', noop, function (err) { + try { + expect(err, 'presence subscribe failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'subscribe failure code'); + callback(); + } catch (err) { + callback(err); + } + }); + }, + function (callback) { + failChan.presence.subscribe('event', noop, function (err) { + try { + expect(err, 'presence unsubscribe failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'subscribe failure code'); + callback(); + } catch (err) { + callback(err); + } + }); + }, + function (callback) { + failChan.presence.get(function (err) { + try { + expect(err, 'presence get failed').to.be.ok; + expect(err.code).to.equal(channelFailedCode, 'presence get failure code'); + callback(); + } catch (err) { + callback(err); } - test.done(); + }); } - ); - } catch(e) { - test.ok(false, 'connection failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - /* - * Check operations on a failed channel give the right errors - */ - exports.failed_channel = function(test) { - test.expect(16); - var realtime = helper.AblyRealtime(); - var failChan; - var channelFailedCode = 90001; - - var tests = [ - function(callback) { - failChan.publish('event', 'data', function(err) { - test.ok(err, "publish failed"); - test.equal(err.code, channelFailedCode, "publish failure code"); - callback(); - }); - }, - function(callback) { - failChan.subscribe('event', noop, function(err) { - test.ok(err, "subscribe failed"); - test.equal(err.code, channelFailedCode, "subscribe failure code"); - callback(); - }); - }, - function(callback) { - failChan.presence.enterClient('clientId', function(err) { - test.ok(err, "presence enter failed"); - test.equal(err.code, channelFailedCode, "presence enter failure code"); - callback(); - }); - }, - function(callback) { - failChan.presence.leaveClient('clientId', function(err) { - test.ok(err, "presence leave failed"); - test.equal(err.code, channelFailedCode, "presence leave failure code"); - callback(); - }); - }, - function(callback) { - failChan.presence.subscribe('event', noop, function(err) { - test.ok(err, "presence subscribe failed"); - test.equal(err.code, channelFailedCode, "subscribe failure code"); - callback(); - }); - }, - function(callback) { - failChan.presence.subscribe('event', noop, function(err) { - test.ok(err, "presence unsubscribe failed"); - test.equal(err.code, channelFailedCode, "subscribe failure code"); - callback(); - }); - }, - function(callback) { - failChan.presence.get(function(err) { - test.ok(err, "presence get failed"); - test.equal(err.code, channelFailedCode, "presence get failure code"); - callback(); - }); - } - ]; - - try { - realtime.connection.once('connected', function() { - failChan = realtime.channels.get("::"); - failChan.attach(function(err) { - test.ok(err, "channel attach failed"); - test.equal(failChan.state, "failed", "channel in failed state"); - async.parallel(tests, function() { - closeAndFinish(test, realtime); + ]; + + try { + realtime.connection.once('connected', function () { + failChan = realtime.channels.get('::'); + failChan.attach(function (err) { + try { + expect(err, 'channel attach failed').to.be.ok; + expect(failChan.state).to.equal('failed', 'channel in failed state'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + async.parallel(tests, function (err) { + closeAndFinish(done, realtime, err); + }); }); }); - }); - } catch(e) { - test.ok(false, 'caught exception: ' + e.message + e.stack); - closeAndFinish(test, realtime); - } - }; + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - exports.attach_timeout = function(test) { - var realtime = helper.AblyRealtime({realtimeRequestTimeout: 10, channelRetryTimeout: 10}), - channel = realtime.channels.get('failed_attach'), - originalOnMessage = channel.onMessage.bind(channel); + it('attach_timeout', function (done) { + var realtime = helper.AblyRealtime({ realtimeRequestTimeout: 10, channelRetryTimeout: 10 }), + channel = realtime.channels.get('failed_attach'), + originalOnMessage = channel.onMessage.bind(channel); - channel.onMessage = function(message) { - if(message.action === 11) { return; } + channel.onMessage = function (message) { + if (message.action === 11) { + return; + } originalOnMessage(message); }; - test.expect(4); - realtime.connection.once('connected', function() { - channel.attach(function(err) { - test.equal(err.code, 90007, 'check channel error code'); - test.equal(err.statusCode, 408, 'check timeout statusCode'); - test.equal(channel.state, 'suspended', 'check channel goes into suspended state'); - channel.once(function(stateChange) { - test.equal(stateChange.current, 'attaching', 'check channel tries to attach again'); - closeAndFinish(test, realtime); + realtime.connection.once('connected', function () { + channel.attach(function (err) { + try { + expect(err.code).to.equal(90007, 'check channel error code'); + expect(err.statusCode).to.equal(408, 'check timeout statusCode'); + expect(channel.state).to.equal('suspended', 'check channel goes into suspended state'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + channel.once(function (stateChange) { + try { + expect(stateChange.current).to.equal('attaching', 'check channel tries to attach again'); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); }); }); }); - }; - - /* RTN7c - * Publish a message, then before it receives an ack, disconnect the - * transport, and let the connection go into some terminal failure state. - * Check that the publish callback is called with an error. - */ - function nack_on_connection_failure(failureFn, expectedRealtimeState, expectedNackCode) { - return function(test) { - test.expect(3) - /* Use one transport because stubbing out transport#onProtocolMesage */ - var realtime = helper.AblyRealtime({transports: [helper.bestTransport]}), - channel = realtime.channels.get('nack_on_connection_failure'); - - async.series([ - function(cb) { realtime.connection.once('connected', function() { cb(); }); }, - function(cb) { channel.attach(cb); }, - function(cb) { - var transport = realtime.connection.connectionManager.activeProtocol.transport, - originalOnProtocolMessage = transport.onProtocolMessage; - - transport.onProtocolMessage = function(message) { - /* make sure we don't get an ack! */ - if(message.action !== 1) { - originalOnProtocolMessage.apply(this, arguments); + + /* RTN7c + * Publish a message, then before it receives an ack, disconnect the + * transport, and let the connection go into some terminal failure state. + * Check that the publish callback is called with an error. + */ + function nack_on_connection_failure(failureFn, expectedRealtimeState, expectedNackCode) { + return function (done) { + /* Use one transport because stubbing out transport#onProtocolMesage */ + var realtime = helper.AblyRealtime({ transports: [helper.bestTransport] }), + channel = realtime.channels.get('nack_on_connection_failure'); + + async.series( + [ + function (cb) { + realtime.connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + channel.attach(cb); + }, + function (cb) { + var transport = realtime.connection.connectionManager.activeProtocol.transport, + originalOnProtocolMessage = transport.onProtocolMessage; + + transport.onProtocolMessage = function (message) { + /* make sure we don't get an ack! */ + if (message.action !== 1) { + originalOnProtocolMessage.apply(this, arguments); + } + }; + channel.publish('foo', 'bar', function (err) { + try { + expect(err, 'Publish failed as expected').to.be.ok; + expect(realtime.connection.state).to.equal( + expectedRealtimeState, + 'check realtime state is ' + expectedRealtimeState + ); + expect(err.code).to.equal(expectedNackCode, 'Check error code was ' + expectedNackCode); + cb(); + } catch (err) { + cb(err); + } + }); + helper.Utils.nextTick(function () { + failureFn(realtime); + }); } - }; - channel.publish('foo', 'bar', function(err) { - test.ok(err, 'Publish failed as expected'); - test.equal(realtime.connection.state, expectedRealtimeState, 'check realtime state is ' + expectedRealtimeState); - test.equal(err.code, expectedNackCode, 'Check error code was ' + expectedNackCode); - cb(); - }); - helper.Utils.nextTick(function() { - failureFn(realtime); + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }; + } + + it( + 'nack_on_connection_suspended', + nack_on_connection_failure( + function (realtime) { + helper.becomeSuspended(realtime); + }, + 'suspended', + 80002 + ) + ); + + it( + 'nack_on_connection_failed', + nack_on_connection_failure( + function (realtime) { + realtime.connection.connectionManager.activeProtocol.transport.onProtocolMessage({ + action: 9, + error: { statusCode: 401, code: 40100, message: 'connection failed because reasons' } }); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - closeAndFinish(test, realtime); + }, + 'failed', + 40100 + ) + ); + + it( + 'nack_on_connection_closed', + nack_on_connection_failure( + function (realtime) { + realtime.close(); + }, + 'closed', + 80017 + ) + ); + + it('idle_transport_timeout', function (done) { + var realtime = helper.AblyRealtime({ realtimeRequestTimeout: 100 }), + originalOnProtocolMessage; + + realtime.connection.connectionManager.on('transport.pending', function (transport) { + originalOnProtocolMessage = transport.onProtocolMessage; + transport.onProtocolMessage = function (message) { + if (message.action === 4) { + message.connectionDetails.maxIdleInterval = 100; + } + originalOnProtocolMessage.call(this, message); + }; }); - }; - } - - exports.nack_on_connection_suspended = nack_on_connection_failure( - function(realtime) { helper.becomeSuspended(realtime); }, - 'suspended', - 80002 - ); - - exports.nack_on_connection_failed = nack_on_connection_failure( - function(realtime) { - realtime.connection.connectionManager.activeProtocol.transport.onProtocolMessage({ - action: 9, - error: {statusCode: 401, code: 40100, message: "connection failed because reasons"} - });}, - 'failed', - 40100 - ); - - exports.nack_on_connection_closed = nack_on_connection_failure( - function(realtime) { realtime.close(); }, - 'closed', - 80017 - ); - - exports.idle_transport_timeout = function(test) { - var realtime = helper.AblyRealtime({realtimeRequestTimeout: 100}), - originalOnProtocolMessage; - - test.expect(3); - - realtime.connection.connectionManager.on('transport.pending', function(transport) { - originalOnProtocolMessage = transport.onProtocolMessage; - transport.onProtocolMessage = function(message) { - if(message.action === 4) { - message.connectionDetails.maxIdleInterval = 100; - } - originalOnProtocolMessage.call(this, message); - }; - }); - realtime.connection.once('connected', function() { - realtime.connection.once(function(statechange) { - /* will go to connecting if there's another transport scheduled for activation */ - test.ok(statechange.current === 'disconnected' || statechange.current === 'connecting', 'check connection goes to disconnected/connecting'); - test.equal(statechange.reason.code, 80003, 'check code'); - test.equal(statechange.reason.statusCode, 408, 'check statusCode'); - closeAndFinish(test, realtime); + realtime.connection.once('connected', function () { + realtime.connection.once(function (statechange) { + /* will go to connecting if there's another transport scheduled for activation */ + try { + expect( + statechange.current === 'disconnected' || statechange.current === 'connecting', + 'check connection goes to disconnected/connecting' + ).to.be.ok; + expect(statechange.reason.code).to.equal(80003, 'check code'); + expect(statechange.reason.statusCode).to.equal(408, 'check statusCode'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); }); }); - }; - - /* RTN14d last sentence: Check that if we received a 5xx disconnected, when - * we try again we use a fallback host */ - helper.testOnAllTransports(exports, 'try_fallback_hosts_on_placement_constraint', function(realtimeOpts) { return function(test) { - /* Use the echoserver as a fallback host because it doesn't support - * websockets, so it'll fail to connect, which we can detect */ - var realtime = helper.AblyRealtime(utils.mixin({fallbackHosts: ['echo.ably.io']}, realtimeOpts)), - connection = realtime.connection, - connectionManager = connection.connectionManager; - - test.expect(1); - connection.once('connected', function() { - connection.once('connecting', function() { - connection.once(function(stateChange) { - test.equal(stateChange.current, 'disconnected', 'expect next connection attempt to fail due to using the (bad) fallback host') - closeAndFinish(test, realtime); + + /* RTN14d last sentence: Check that if we received a 5xx disconnected, when + * we try again we use a fallback host */ + helper.testOnAllTransports('try_fallback_hosts_on_placement_constraint', function (realtimeOpts) { + return function (done) { + /* Use the echoserver as a fallback host because it doesn't support + * websockets, so it'll fail to connect, which we can detect */ + var realtime = helper.AblyRealtime(utils.mixin({ fallbackHosts: ['echo.ably.io'] }, realtimeOpts)), + connection = realtime.connection, + connectionManager = connection.connectionManager; + + connection.once('connected', function () { + connection.once('connecting', function () { + connection.once(function (stateChange) { + try { + expect(stateChange.current).to.equal( + 'disconnected', + 'expect next connection attempt to fail due to using the (bad) fallback host' + ); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 6, + error: { + message: 'fake placement constraint', + code: 50320, + statusCode: 503 + } + }) + ); }); - }); - connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 6, - error: { - message: "fake placement constraint", - code: 50320, - statusCode: 503 - } - })); + }; }); - }}); - - // RTL 17 - exports.no_messages_if_not_attached = function(test) { - - var testName = 'no_messages_if_not_attached'; - var testMessage = { foo: 'bar', count: 1, status: 'active' }; - var testMessage2 = { foo: 'bar', count: 2, status: 'active' }; - - test.expect(2); - - try { - - var sender_realtime = helper.AblyRealtime(); - var sender_channel = sender_realtime.channels.get(testName); - - var messageReceived = false; - - sender_channel.subscribe(function(message) { - - if(messageReceived) { - test.ok(false, 'Message received when channel not in ATTACHED state.'); - } - messageReceived = true; - test.deepEqual(testMessage, message.data, 'Check first message received'); - - var connectionManager = sender_realtime.connection.connectionManager; + // RTL 17 + it('no_messages_if_not_attached', function (done) { + var testName = 'no_messages_if_not_attached'; + var testMessage = { foo: 'bar', count: 1, status: 'active' }; + var testMessage2 = { foo: 'bar', count: 2, status: 'active' }; + + try { + var sender_realtime = helper.AblyRealtime(); + var sender_channel = sender_realtime.channels.get(testName); + var messageReceived = false; - var onChannelMsgOrig = connectionManager.onChannelMessage; - connectionManager.onChannelMessage = function(msg, transport) { - if(msg.action === 15) { - sender_channel.requestState('attaching'); + sender_channel.subscribe(function (message) { + if (messageReceived) { + closeAndFinish(done, realtime, new Error('Message received when channel not in ATTACHED state.')); } - onChannelMsgOrig.call(connectionManager, msg, transport); - }; - - sender_channel.publish('1', testMessage2); - - var success = setTimeout(() => { - test.ok(true); - closeAndFinish(test, sender_realtime); - }, 7000); - - }); - sender_realtime.connection.on('connected', function() { - sender_channel.publish('0', testMessage); - }); - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, sender_realtime); } - }; - - helper.withMocha('realtime/failure', exports); -}); \ No newline at end of file + try { + messageReceived = true; + expect(testMessage).to.deep.equal(message.data, 'Check first message received'); + + var connectionManager = sender_realtime.connection.connectionManager; + var onChannelMsgOrig = connectionManager.onChannelMessage; + connectionManager.onChannelMessage = function (msg, transport) { + if (msg.action === 15) { + sender_channel.requestState('attaching'); + } + onChannelMsgOrig.call(connectionManager, msg, transport); + }; + + sender_channel.publish('1', testMessage2); + + setTimeout(function () { + closeAndFinish(done, sender_realtime); + }, 7000); + } catch (err) { + closeAndFinish(done, sender_realtime, err); + } + }); + + sender_realtime.connection.on('connected', function () { + sender_channel.publish('0', testMessage); + }); + } catch (err) { + closeAndFinish(done, sender_realtime, err); + } + }); + }); +}); + diff --git a/spec/realtime/history.test.js b/spec/realtime/history.test.js index 006d03378a..6b47358d61 100644 --- a/spec/realtime/history.test.js +++ b/spec/realtime/history.test.js @@ -1,132 +1,149 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var rest, exports = {}, - displayError = helper.displayError, - utils = helper.Utils, - preAttachMessages = utils.arrMap([1,2,3,4,5], function(i) { - return { name: 'pre-attach-' + i, - data: 'some data' } - }), - postAttachMessages = utils.arrMap([1,2,3,4,5], function(i) { - return { name: 'post-attach-' + i, - data: 'some data' } - }), - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection; +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var expect = chai.expect; + var utils = helper.Utils; + var preAttachMessages = utils.arrMap([1, 2, 3, 4, 5], function (i) { + return { name: 'pre-attach-' + i, data: 'some data' }; + }); + var postAttachMessages = utils.arrMap([1, 2, 3, 4, 5], function (i) { + return { name: 'post-attach-' + i, data: 'some data' }; + }); + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; - var parallelPublishMessages = function(test, channel, messages, callback) { - var publishTasks = utils.arrMap(messages, function(event) { - return function(publishCb) { + var parallelPublishMessages = function (done, channel, messages, callback) { + var publishTasks = utils.arrMap(messages, function (event) { + return function (publishCb) { channel.publish(event.name, event.data, publishCb); }; }); try { - async.parallel(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + async.parallel(publishTasks, function (err) { + if (err) { + done(err); return; } callback(); }); - } catch(e) { - console.log(e.stack); + } catch (err) { + done(err); } }; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); - }; - - exports.history_until_attach = function(test) { - test.expect(4); - var rest = helper.AblyRest(); - var realtime = helper.AblyRealtime(); - var restChannel = rest.channels.get('persisted:history_until_attach'); + describe('realtime/history', function () { + this.timeout(60 * 1000); - /* first, send a number of events to this channel before attaching */ - parallelPublishMessages(test, restChannel, preAttachMessages, function(){ + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); + }); - /* second, connect and attach to the channel */ - try { - realtime.connection.whenState('connected', function() { - var rtChannel = realtime.channels.get('persisted:history_until_attach'); - rtChannel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - return; - } + it('history_until_attach', function (done) { + var rest = helper.AblyRest(); + var realtime = helper.AblyRealtime(); + var restChannel = rest.channels.get('persisted:history_until_attach'); - /* third, send some more events post-attach (over rest, not using the - * new realtime connection) */ + /* first, send a number of events to this channel before attaching */ + parallelPublishMessages(done, restChannel, preAttachMessages, function () { + /* second, connect and attach to the channel */ + try { + realtime.connection.whenState('connected', function () { + var rtChannel = realtime.channels.get('persisted:history_until_attach'); + rtChannel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - parallelPublishMessages(test, restChannel, postAttachMessages, function(){ + /* third, send some more events post-attach (over rest, not using the + * new realtime connection) */ - /* fourth, query history using the realtime connection with - * untilAttach both true, false, and not present, checking that - * the right messages are returned in each case */ + parallelPublishMessages(done, restChannel, postAttachMessages, function () { + /* fourth, query history using the realtime connection with + * untilAttach both true, false, and not present, checking that + * the right messages are returned in each case */ - var tests = [ - function(callback) { - rtChannel.history(function(err, resultPage) { - if(err) { callback(err); } - var expectedLength = preAttachMessages.length + postAttachMessages.length - test.equal(resultPage.items.length, expectedLength, 'Verify all messages returned when no params'); - callback(); - }); - }, - function(callback) { - rtChannel.history({untilAttach: false}, function(err, resultPage) { - if(err) { callback(err); } - var expectedLength = preAttachMessages.length + postAttachMessages.length - test.equal(resultPage.items.length, expectedLength, 'Verify all messages returned when untilAttached is false'); - callback(); - }); - }, - function(callback) { - rtChannel.history({untilAttach: true}, function(err, resultPage) { - if(err) { callback(err); } + var tests = [ + function (callback) { + rtChannel.history(function (err, resultPage) { + if (err) { + callback(err); + } + try { + var expectedLength = preAttachMessages.length + postAttachMessages.length; + expect(resultPage.items.length).to.equal( + expectedLength, + 'Verify all messages returned when no params' + ); + callback(); + } catch (err) { + callback(err); + } + }); + }, + function (callback) { + rtChannel.history({ untilAttach: false }, function (err, resultPage) { + if (err) { + callback(err); + } + try { + var expectedLength = preAttachMessages.length + postAttachMessages.length; + expect(resultPage.items.length).to.equal( + expectedLength, + 'Verify all messages returned when untilAttached is false' + ); + callback(); + } catch (err) { + callback(err); + } + }); + }, + function (callback) { + rtChannel.history({ untilAttach: true }, function (err, resultPage) { + if (err) { + callback(err); + } - /* verify only the pre-attached messages are received */ - var messages = resultPage.items; - test.equal(messages.length, preAttachMessages.length, 'Verify right number of messages returned when untilAttached is true'); - test.ok(utils.arrEvery(messages, function(message) { - return message.name.substring(0,10) == "pre-attach"; - }), "Verify all returned messages were pre-attach ones") - callback(); - }); - } - ] + try { + /* verify only the pre-attached messages are received */ + var messages = resultPage.items; + expect(messages.length).to.equal( + preAttachMessages.length, + 'Verify right number of messages returned when untilAttached is true' + ); + expect( + utils.arrEvery(messages, function (message) { + return message.name.substring(0, 10) == 'pre-attach'; + }), + 'Verify all returned messages were pre-attach ones' + ).to.be.ok; + callback(); + } catch (err) { + callback(err); + } + }); + } + ]; - async.parallel(tests, function(err){ - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); - return; - } - closeAndFinish(test, realtime); - }) + async.parallel(tests, function (err) { + closeAndFinish(done, realtime, err); + }); + }); }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); }); - }; + }); +}); - helper.withMocha('realtime/history', exports); -}); \ No newline at end of file diff --git a/spec/realtime/init.test.js b/spec/realtime/init.test.js index 35e56ff0b9..102bbb5455 100644 --- a/spec/realtime/init.test.js +++ b/spec/realtime/init.test.js @@ -1,378 +1,441 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection; +define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); + describe('realtime/init', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); }); - }; - /* - * Base init case - */ - exports.initbase0 = function(test) { - var realtime; - try { - /* Restrict to websocket or xhr streaming for the v= test as if stream=false the - * recvRequest may not be the connectRequest by the time we check it. All comet - * transports share the same connect uri generation code so should be adequately - * tested by testing xhr_streaming */ - if(helper.bestTransport !== 'web_socket' && helper.bestTransport !== 'xhr_streaming') { - test.done(); - return; - } - realtime = helper.AblyRealtime({transports: ['web_socket', 'xhr_streaming']}); - realtime.connection.on('connected', function() { - test.ok(true, 'Verify init with key'); - /* check api version */ - var transport = realtime.connection.connectionManager.activeProtocol.transport; - var connectUri = helper.isWebsocket(transport) ? transport.uri : transport.recvRequest.uri; - test.ok(connectUri.indexOf('v=1.2') > -1, 'Check uri includes v=1.2'); - closeAndFinish(test, realtime); + /* Restrict to websocket or xhr streaming for the v= test as if stream=false the + * recvRequest may not be the connectRequest by the time we check it. All comet + * transports share the same connect uri generation code so should be adequately + * tested by testing xhr_streaming */ + if (helper.bestTransport === 'web_socket' || helper.bestTransport === 'xhr_streaming') { + /* + * Base init case + */ + it('initbase0', function (done) { + var realtime; + try { + realtime = helper.AblyRealtime({ transports: ['web_socket', 'xhr_streaming'] }); + realtime.connection.on('connected', function () { + /* check api version */ + var transport = realtime.connection.connectionManager.activeProtocol.transport; + var connectUri = helper.isWebsocket(transport) ? transport.uri : transport.recvRequest.uri; + try { + expect(connectUri.indexOf('v=1.2') > -1, 'Check uri includes v=1.2').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Init with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); } - }; - - /* init with key string */ - exports.init_key_string = function(test) { - test.expect(2); - var realtime; - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - realtime = new helper.Ably.Realtime(keyStr); - test.equal(realtime.options.key, keyStr); - test.deepEqual(realtime.options, realtime.connection.connectionManager.options); - closeAndFinish(test, realtime); - } catch(e) { - test.ok(false, 'Init with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - - /* init with token string */ - exports.init_token_string = function(test) { - test.expect(2); - try { - /* first generate a token ... */ - var rest = helper.AblyRest(); - var testKeyOpts = {key: helper.getTestApp().keys[1].keyStr}; + /* init with key string */ + it('init_key_string', function (done) { + var realtime; + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + realtime = new helper.Ably.Realtime(keyStr); - rest.auth.requestToken(null, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); + try { + expect(realtime.options.key).to.equal(keyStr); + expect(realtime.options).to.deep.equal(realtime.connection.connectionManager.options); + } catch (err) { + closeAndFinish(done, realtime, err); return; } + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - var tokenStr = tokenDetails.token, - realtime = new helper.Ably.Realtime(tokenStr); + /* init with token string */ + it('init_token_string', function (done) { + try { + /* first generate a token ... */ + var rest = helper.AblyRest(); + var testKeyOpts = { key: helper.getTestApp().keys[1].keyStr }; - test.equal(realtime.options.token, tokenStr); - test.deepEqual(realtime.options, realtime.connection.connectionManager.options); - closeAndFinish(test, realtime); - }); - } catch(e) { - test.ok(false, 'Init with token failed with exception: ' + e.stack); - test.done(); - } - }; + rest.auth.requestToken(null, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } - /* init with key string and useTokenAuth: true */ - exports.init_key_with_usetokenauth = function(test) { - test.expect(4); - var realtime; - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - realtime = helper.AblyRealtime({key: keyStr, useTokenAuth: true}); - test.equal(realtime.options.key, keyStr); - test.equal(realtime.auth.method, 'token'); - test.equal(realtime.auth.clientId, undefined); - /* Check that useTokenAuth by default results in an anonymous (and not wildcard) token */ - realtime.connection.on('connected', function() { - test.equal(realtime.auth.tokenDetails.clientId, undefined); - closeAndFinish(test, realtime); - }); - } catch(e) { - test.ok(false, 'Init with key and usetokenauth failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + var tokenStr = tokenDetails.token, + realtime = new helper.Ably.Realtime(tokenStr); - /* init with key string, useTokenAuth: true, and some defaultTokenParams to - * request a wildcard clientId */ - exports.init_usetokenauth_defaulttokenparams_wildcard = function(test) { - test.expect(4); - var realtime; - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - realtime = helper.AblyRealtime({key: keyStr, useTokenAuth: true, defaultTokenParams: {clientId: '*', ttl: 12345}}); - test.equal(realtime.auth.clientId, undefined); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.tokenDetails.clientId, '*'); - /* auth.clientId now does inherit the value '*' -- RSA7b4 */ - test.equal(realtime.auth.clientId, '*'); - test.equal(realtime.auth.tokenDetails.expires - realtime.auth.tokenDetails.issued, 12345); - closeAndFinish(test, realtime); - }); - } catch(e) { - test.ok(false, 'init_usetokenauth_defaulttokenparams_wildcard failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + try { + expect(realtime.options.token).to.equal(tokenStr); + expect(realtime.options).to.deep.equal(realtime.connection.connectionManager.options); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + } catch (err) { + done(err); + } + }); - /* init with using defaultTokenParams to set a non-wildcard clientId should set auth.clientId */ - exports.init_defaulttokenparams_nonwildcard = function(test) { - test.expect(3); - var realtime; - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - realtime = helper.AblyRealtime({key: keyStr, useTokenAuth: true, defaultTokenParams: {clientId: 'test'}}); - test.equal(realtime.auth.clientId, undefined); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.tokenDetails.clientId, 'test'); - test.equal(realtime.auth.clientId, 'test'); - closeAndFinish(test, realtime); - }); - } catch(e) { - test.ok(false, 'init_defaulttokenparams_nonwildcard failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + /* init with key string and useTokenAuth: true */ + it('init_key_with_usetokenauth', function (done) { + var realtime; + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + realtime = helper.AblyRealtime({ key: keyStr, useTokenAuth: true }); + expect(realtime.options.key).to.equal(keyStr); + expect(realtime.auth.method).to.equal('token'); + expect(realtime.auth.clientId).to.equal(undefined); + /* Check that useTokenAuth by default results in an anonymous (and not wildcard) token */ + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.tokenDetails.clientId).to.equal(undefined); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* init when specifying clientId both in defaultTokenParams and in clientOptions: the latter takes precedence */ - exports.init_conflicting_clientids = function(test) { - test.expect(2); - var realtime; - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - realtime = helper.AblyRealtime({key: keyStr, useTokenAuth: true, clientId: 'yes', defaultTokenParams: {clientId: 'no'}}); - realtime.connection.on('connected', function() { - test.equal(realtime.auth.tokenDetails.clientId, 'yes'); - test.equal(realtime.auth.clientId, 'yes'); - closeAndFinish(test, realtime); - }); - } catch(e) { - test.ok(false, 'init_conflicting_clientids failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + /* init with key string, useTokenAuth: true, and some defaultTokenParams to + * request a wildcard clientId */ + it('init_usetokenauth_defaulttokenparams_wildcard', function (done) { + var realtime; + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + realtime = helper.AblyRealtime({ + key: keyStr, + useTokenAuth: true, + defaultTokenParams: { clientId: '*', ttl: 12345 } + }); + expect(realtime.auth.clientId).to.equal(undefined); + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.tokenDetails.clientId).to.equal('*'); + /* auth.clientId now does inherit the value '*' -- RSA7b4 */ + expect(realtime.auth.clientId).to.equal('*'); + expect(realtime.auth.tokenDetails.expires - realtime.auth.tokenDetails.issued).to.equal(12345); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* init with useTokenAuth: false with a clientId (should fail) */ - exports.init_with_usetokenauth_false_and_a_clientid = function(test) { - test.expect(1); - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - test.throws(function(){ - realtime = new helper.Ably.Realtime({key: keyStr, useTokenAuth: false, clientId: "foo"}); - }); - test.done(); - } catch(e) { - test.ok(false, 'Init with key failed with exception: ' + e.stack); - test.done(); - } - }; + /* init with using defaultTokenParams to set a non-wildcard clientId should set auth.clientId */ + it('init_defaulttokenparams_nonwildcard', function (done) { + var realtime; + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + realtime = helper.AblyRealtime({ key: keyStr, useTokenAuth: true, defaultTokenParams: { clientId: 'test' } }); + expect(realtime.auth.clientId).to.equal(undefined); + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.tokenDetails.clientId).to.equal('test'); + expect(realtime.auth.clientId).to.equal('test'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* check default httpHost selection */ - exports.init_defaulthost = function(test) { - test.expect(1); - try { - /* want to check the default host when no custom environment or custom - * host set, so not using helpers.realtime this time, which will use a - * test env */ - var realtime = new Ably.Realtime({ key: 'not_a.real:key', autoConnect: false }); - var defaultHost = realtime.connection.connectionManager.httpHosts[0]; - test.equal(defaultHost, 'rest.ably.io', 'Verify correct default rest host chosen'); - realtime.close(); - test.done(); - } catch(e) { - test.ok(false, 'Init with key failed with exception: ' + e.stack); - test.done(); - } - }; + /* init when specifying clientId both in defaultTokenParams and in clientOptions: the latter takes precedence */ + it('init_conflicting_clientids', function (done) { + var realtime; + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + realtime = helper.AblyRealtime({ + key: keyStr, + useTokenAuth: true, + clientId: 'yes', + defaultTokenParams: { clientId: 'no' } + }); + realtime.connection.on('connected', function () { + try { + expect(realtime.auth.tokenDetails.clientId).to.equal('yes'); + expect(realtime.auth.clientId).to.equal('yes'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* check changing the default timeouts */ - exports.init_timeouts = function(test) { - test.expect(3); - try { - var realtime = helper.AblyRealtime({ - key: 'not_a.real:key', - disconnectedRetryTimeout: 123, - suspendedRetryTimeout: 456, - httpRequestTimeout: 789 - }); - /* Note: uses internal knowledge of connectionManager */ - test.equal(realtime.connection.connectionManager.states.disconnected.retryDelay, 123, 'Verify disconnected retry frequency is settable'); - test.equal(realtime.connection.connectionManager.states.suspended.retryDelay, 456, 'Verify suspended retry frequency is settable'); - test.equal(realtime.connection.connectionManager.options.timeouts.httpRequestTimeout, 789, 'Verify suspended retry frequency is settable'); - closeAndFinish(test, realtime); - } catch(e) { - test.ok(false, 'init_defaulthost failed with exception: ' + e.stack); - test.done(); - } - }; + /* init with useTokenAuth: false with a clientId (should fail) */ + it('init_with_usetokenauth_false_and_a_clientid', function (done) { + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + expect(function () { + realtime = new helper.Ably.Realtime({ key: keyStr, useTokenAuth: false, clientId: 'foo' }); + }).to.throw; + done(); + } catch (err) { + done(err); + } + }); - /* check changing the default fallback hosts and changing httpMaxRetryCount */ - exports.init_fallbacks = function(test) { - test.expect(6); - try { - var realtime = helper.AblyRealtime({ - key: 'not_a.real:key', - restHost: 'a', - httpMaxRetryCount: 2, - autoConnect: false, - fallbackHosts: ['b', 'c', 'd', 'e'] - }); - /* Note: uses internal knowledge of connectionManager */ - test.equal(realtime.connection.connectionManager.httpHosts.length, 3, 'Verify hosts list is the expected length'); - test.equal(realtime.connection.connectionManager.httpHosts[0], 'a', 'Verify given restHost is first'); - /* Replace chooseTransportForHost with a spy, then try calling - * chooseHttpTransport to see what host is picked */ - realtime.connection.connectionManager.tryATransport = function(transportParams, transport, cb) { - switch(transportParams.host) { - case 'a': - test.ok(true, 'Tries first with restHost'); - cb(false); - break; - case 'b': - case 'c': - case 'd': - case 'e': - /* should be called twice */ - test.ok(true, 'Tries each of the fallback hosts in turn'); - cb(false); - } - }; - realtime.connection.on('disconnected', function(stateChange) { - test.equal(stateChange.reason.code, 80003, 'Expected error code after no fallback host works'); - closeAndFinish(test, realtime); - }) - realtime.connection.connect(); - } catch(e) { - test.ok(false, 'init_defaulthost failed with exception: ' + e.stack); - test.done(); - } - } + /* check default httpHost selection */ + it('init_defaulthost', function (done) { + try { + /* want to check the default host when no custom environment or custom + * host set, so not using helpers.realtime this time, which will use a + * test env */ + var realtime = new Ably.Realtime({ key: 'not_a.real:key', autoConnect: false }); + var defaultHost = realtime.connection.connectionManager.httpHosts[0]; + expect(defaultHost).to.equal('rest.ably.io', 'Verify correct default rest host chosen'); + realtime.close(); + done(); + } catch (err) { + done(err); + } + }); - /* Check base and upgrade transports (nodejs only; browser tests in their own section) */ - if(!isBrowser) { - exports.node_transports = function(test) { - test.expect(2); - var realtime; + /* check changing the default timeouts */ + it('init_timeouts', function (done) { try { - realtime = helper.AblyRealtime({transports: helper.availableTransports}); - test.equal(realtime.connection.connectionManager.baseTransport, 'comet'); - test.deepEqual(realtime.connection.connectionManager.upgradeTransports, ['web_socket']); - closeAndFinish(test, realtime); - } catch(e) { - test.ok(false, 'Init with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); + var realtime = helper.AblyRealtime({ + key: 'not_a.real:key', + disconnectedRetryTimeout: 123, + suspendedRetryTimeout: 456, + httpRequestTimeout: 789 + }); + /* Note: uses internal knowledge of connectionManager */ + try { + expect(realtime.connection.connectionManager.states.disconnected.retryDelay).to.equal( + 123, + 'Verify disconnected retry frequency is settable' + ); + expect(realtime.connection.connectionManager.states.suspended.retryDelay).to.equal( + 456, + 'Verify suspended retry frequency is settable' + ); + expect(realtime.connection.connectionManager.options.timeouts.httpRequestTimeout).to.equal( + 789, + 'Verify suspended retry frequency is settable' + ); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } catch (err) { + done(err); } - }; - } + }); - /* Check that the connectionKey in ConnectionDetails takes precedence over connectionKey in ProtocolMessage, - and clientId in ConnectionDetails updates the client clientId */ - exports.init_and_connection_details = function(test) { - test.expect(4); - try { - var keyStr = helper.getTestApp().keys[0].keyStr; - var realtime = helper.AblyRealtime({ key: keyStr, useTokenAuth: true }); - realtime.connection.connectionManager.once('transport.pending', function (state) { - var transport = realtime.connection.connectionManager.pendingTransports[0], - originalOnProtocolMessage = transport.onProtocolMessage; - realtime.connection.connectionManager.pendingTransports[0].onProtocolMessage = function(message) { - if(message.action === 4) { - test.ok(message.connectionDetails.connectionKey); - test.equal(message.connectionDetails.connectionKey, message.connectionKey, 'connection keys should match'); - message.connectionDetails.connectionKey = 'importantConnectionKey'; - message.connectionDetails.clientId = 'customClientId'; + /* check changing the default fallback hosts and changing httpMaxRetryCount */ + it('init_fallbacks', function (done) { + try { + var realtime = helper.AblyRealtime({ + key: 'not_a.real:key', + restHost: 'a', + httpMaxRetryCount: 2, + autoConnect: false, + fallbackHosts: ['b', 'c', 'd', 'e'] + }); + /* Note: uses internal knowledge of connectionManager */ + expect(realtime.connection.connectionManager.httpHosts.length).to.equal( + 3, + 'Verify hosts list is the expected length' + ); + expect(realtime.connection.connectionManager.httpHosts[0]).to.equal('a', 'Verify given restHost is first'); + /* Replace chooseTransportForHost with a spy, then try calling + * chooseHttpTransport to see what host is picked */ + realtime.connection.connectionManager.tryATransport = function (transportParams, transport, cb) { + switch (transportParams.host) { + case 'a': + cb(false); + break; + case 'b': + case 'c': + case 'd': + case 'e': + /* should be called twice */ + cb(false); } - originalOnProtocolMessage.call(transport, message); }; + realtime.connection.on('disconnected', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(80003, 'Expected error code after no fallback host works'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + realtime.connection.connect(); + } catch (err) { + done(err); + } + }); + + /* Check base and upgrade transports (nodejs only; browser tests in their own section) */ + if (!isBrowser) { + it('node_transports', function (done) { + var realtime; + try { + realtime = helper.AblyRealtime({ transports: helper.availableTransports }); + expect(realtime.connection.connectionManager.baseTransport).to.equal('comet'); + expect(realtime.connection.connectionManager.upgradeTransports).to.deep.equal(['web_socket']); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } }); - realtime.connection.once('connected', function() { - test.equal(realtime.auth.clientId, 'customClientId', 'clientId should be set on the Auth object from connectionDetails'); - test.equal(realtime.connection.key, 'importantConnectionKey', 'connection key from connectionDetails should be used'); - test.done(); - realtime.close(); - }); - } catch(e) { - test.ok(false, 'Init with token failed with exception: ' + e.stack); - test.done(); } - }; - exports.init_fallbacks_once_connected = function(test) { - var realtime = helper.AblyRealtime({ - httpMaxRetryCount: 3, - fallbackHosts: ['a', 'b', 'c'] + /* Check that the connectionKey in ConnectionDetails takes precedence over connectionKey in ProtocolMessage, + and clientId in ConnectionDetails updates the client clientId */ + it('init_and_connection_details', function (done) { + try { + var keyStr = helper.getTestApp().keys[0].keyStr; + var realtime = helper.AblyRealtime({ key: keyStr, useTokenAuth: true }); + realtime.connection.connectionManager.once('transport.pending', function (state) { + var transport = realtime.connection.connectionManager.pendingTransports[0], + originalOnProtocolMessage = transport.onProtocolMessage; + realtime.connection.connectionManager.pendingTransports[0].onProtocolMessage = function (message) { + try { + if (message.action === 4) { + expect(message.connectionDetails.connectionKey).to.be.ok; + expect(message.connectionDetails.connectionKey).to.equal( + message.connectionKey, + 'connection keys should match' + ); + message.connectionDetails.connectionKey = 'importantConnectionKey'; + message.connectionDetails.clientId = 'customClientId'; + } + originalOnProtocolMessage.call(transport, message); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }; + }); + realtime.connection.once('connected', function () { + try { + expect(realtime.auth.clientId).to.equal( + 'customClientId', + 'clientId should be set on the Auth object from connectionDetails' + ); + expect(realtime.connection.key).to.equal( + 'importantConnectionKey', + 'connection key from connectionDetails should be used' + ); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + } catch (err) { + done(err); + } }); - realtime.connection.once('connected', function() { - var hosts = Ably.Rest.Http._getHosts(realtime); - /* restHost rather than realtimeHost as that's what connectionManager - * knows about; converted to realtimeHost by the websocketTransport */ - test.equal(hosts[0], realtime.options.restHost, 'Check connected realtime host is the first option'); - test.equal(hosts.length, 4, 'Check also have three fallbacks'); - closeAndFinish(test, realtime); - }) - }; - exports.init_fallbacks_once_connected_2 = function(test) { - var goodHost = helper.AblyRest().options.realtimeHost; - var realtime = helper.AblyRealtime({ - httpMaxRetryCount: 3, - restHost: 'a', - fallbackHosts: [goodHost, 'b', 'c'] + it('init_fallbacks_once_connected', function (done) { + var realtime = helper.AblyRealtime({ + httpMaxRetryCount: 3, + fallbackHosts: ['a', 'b', 'c'] + }); + realtime.connection.once('connected', function () { + try { + var hosts = Ably.Rest.Http._getHosts(realtime); + /* restHost rather than realtimeHost as that's what connectionManager + * knows about; converted to realtimeHost by the websocketTransport */ + expect(hosts[0]).to.equal(realtime.options.restHost, 'Check connected realtime host is the first option'); + expect(hosts.length).to.equal(4, 'Check also have three fallbacks'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); }); - realtime.connection.once('connected', function() { - var hosts = Ably.Rest.Http._getHosts(realtime); - /* restHost rather than realtimeHost as that's what connectionManager - * knows about; converted to realtimeHost by the websocketTransport */ - test.equal(hosts[0], goodHost, 'Check connected realtime host is the first option'); - closeAndFinish(test, realtime); - }) - } - exports.init_callbacks_promises = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; - } + it('init_fallbacks_once_connected_2', function (done) { + var goodHost = helper.AblyRest().options.realtimeHost; + var realtime = helper.AblyRealtime({ + httpMaxRetryCount: 3, + restHost: 'a', + fallbackHosts: [goodHost, 'b', 'c'] + }); + realtime.connection.once('connected', function () { + var hosts = Ably.Rest.Http._getHosts(realtime); + /* restHost rather than realtimeHost as that's what connectionManager + * knows about; converted to realtimeHost by the websocketTransport */ + try { + expect(hosts[0]).to.equal(goodHost, 'Check connected realtime host is the first option'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); - var realtime, - keyStr = helper.getTestApp().keys[0].keyStr, - getOptions = function() { return {key: keyStr, autoConnect: false}; }; + if (typeof Promise === 'undefined') { + it('init_callbacks_promises', function (done) { + try { + var realtime, + keyStr = helper.getTestApp().keys[0].keyStr, + getOptions = function () { + return { key: keyStr, autoConnect: false }; + }; - realtime = new Ably.Realtime(getOptions()); - test.ok(!realtime.options.promises, 'Check promises defaults to false'); + realtime = new Ably.Realtime(getOptions()); + expect(!realtime.options.promises, 'Check promises defaults to false').to.be.ok; - realtime = new Ably.Realtime.Promise(getOptions()); - test.ok(realtime.options.promises, 'Check promises default to true with promise constructor'); + realtime = new Ably.Realtime.Promise(getOptions()); + expect(realtime.options.promises, 'Check promises default to true with promise constructor').to.be.ok; - if(!isBrowser && typeof require == 'function') { - realtime = new require('../../promises').Realtime(getOptions()); - test.ok(realtime.options.promises, 'Check promises default to true with promise require target'); + if (!isBrowser && typeof require == 'function') { + realtime = new require('../../promises').Realtime(getOptions()); + expect(realtime.options.promises, 'Check promises default to true with promise require target').to.be.ok; - realtime = new require('../../callbacks').Realtime(getOptions()); - test.ok(!realtime.options.promises, 'Check promises default to false with callback require target'); + realtime = new require('../../callbacks').Realtime(getOptions()); + expect(!realtime.options.promises, 'Check promises default to false with callback require target').to.be.ok; + } + done(); + } catch (err) { + done(err); + } + }); } - test.done(); - }; - - helper.withMocha('realtime/init', exports); -}); \ No newline at end of file + }); +}); diff --git a/spec/realtime/message.test.js b/spec/realtime/message.test.js index 91ce132b1e..de5630996c 100644 --- a/spec/realtime/message.test.js +++ b/spec/realtime/message.test.js @@ -1,965 +1,1134 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - _exports = {}, - displayError = helper.displayError, - utils = helper.Utils, - closeAndFinish = helper.closeAndFinish, - createPM = Ably.Realtime.ProtocolMessage.fromDeserialized, - monitorConnection = helper.monitorConnection, - testOnAllTransports = helper.testOnAllTransports; - - var publishIntervalHelper = function(currentMessageNum, channel, dataFn, onPublish){ - return function(currentMessageNum) { - console.log('sending: ' + currentMessageNum); - channel.publish('event0', dataFn(), function() { - console.log('publish callback called'); - onPublish(); - }); - }; - }, - publishAtIntervals = function(numMessages, channel, dataFn, onPublish){ - for(var i = numMessages; i > 0; i--) { - setTimeout(publishIntervalHelper(i, channel, dataFn, onPublish), 20*i); - } +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + var displayError = helper.displayError; + var utils = helper.Utils; + var closeAndFinish = helper.closeAndFinish; + var createPM = Ably.Realtime.ProtocolMessage.fromDeserialized; + var monitorConnection = helper.monitorConnection; + var testOnAllTransports = helper.testOnAllTransports; + + var publishIntervalHelper = function (currentMessageNum, channel, dataFn, onPublish) { + return function () { + channel.publish('event0', dataFn(), function () { + onPublish(); + }); }; + }; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, displayError(err)); - } else { - test.ok(true, 'setup app'); - } - test.done(); - }); + var publishAtIntervals = function (numMessages, channel, dataFn, onPublish) { + for (var i = numMessages; i > 0; i--) { + setTimeout(publishIntervalHelper(i, channel, dataFn, onPublish), 20 * i); + } }; - exports.publishonce = function(test) { - test.expect(2); - try { - /* set up realtime */ - var realtime = helper.AblyRealtime(); - var rest = helper.AblyRest(); + describe('realtime/message', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); - /* connect and attach */ - realtime.connection.on('connected', function() { - var testMsg = 'Hello world'; - var rtChannel = realtime.channels.get('publishonce'); - rtChannel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - return; - } + it('publishonce', function (done) { + try { + /* set up realtime */ + var realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + + /* connect and attach */ + realtime.connection.on('connected', function () { + var testMsg = 'Hello world'; + var rtChannel = realtime.channels.get('publishonce'); + rtChannel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - /* subscribe to event */ - rtChannel.subscribe('event0', function(msg) { - test.ok(true, 'Received event0'); - test.equal(msg.data, testMsg, 'Unexpected msg text received'); - closeAndFinish(test, realtime); - }); + /* subscribe to event */ + rtChannel.subscribe('event0', function (msg) { + try { + expect(msg.data).to.equal(testMsg, 'Unexpected msg text received'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); - /* publish event */ - var restChannel = rest.channels.get('publishonce'); - restChannel.publish('event0', testMsg); + /* publish event */ + var restChannel = rest.channels.get('publishonce'); + restChannel.publish('event0', testMsg); + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* - * Test publishes in quick succession (on successive ticks of the event loop) - */ - testOnAllTransports(exports, 'publishfast', function(realtimeOpts) { return function(test) { - test.expect(100); - try { - var realtime = helper.AblyRealtime(realtimeOpts); - realtime.connection.once('connected', function() { - var channel = realtime.channels.get('publishfast_' + String(Math.random()).substr(2)); - channel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - return; - } + /* + * Test publishes in quick succession (on successive ticks of the event loop) + */ + testOnAllTransports('publishfast', function (realtimeOpts) { + return function (done) { + try { + var realtime = helper.AblyRealtime(realtimeOpts); + realtime.connection.once('connected', function () { + var channel = realtime.channels.get('publishfast_' + String(Math.random()).substr(2)); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - async.parallel([ - function(cb) { - channel.subscribe('event', function(msg) { - test.ok(true, 'Received event ' + msg.data); - if(msg.data === '49') { - cb(); + async.parallel( + [ + function (cb) { + channel.subscribe('event', function (msg) { + if (msg.data === '49') { + cb(); + } + }); + }, + function (cb) { + var ackd = 0; + var publish = function (i) { + channel.publish('event', i.toString(), function (err) { + try { + expect( + !err, + 'successfully published ' + i + (err ? ' err was ' + displayError(err) : '') + ).to.be.ok; + } catch (err) { + cb(err); + return; + } + ackd++; + if (ackd === 50) cb(); + }); + if (i < 49) { + setTimeout(function () { + publish(i + 1); + }, 0); + } + }; + publish(0); + } + ], + function (err) { + closeAndFinish(done, realtime, err); } - }); - }, - function(cb) { - var ackd = 0; - var publish = function(i) { - channel.publish('event', i.toString(), function(err) { - test.ok(!err, 'successfully published ' + i + (err ? ' err was ' + displayError(err) : '')); - ackd++; - if(ackd === 50) cb(); - }); - if(i < 49) { - setTimeout(function() { - publish(i + 1); - }, 0); - } - }; - publish(0); - } - ], function() { - closeAndFinish(test, realtime); - }); - }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - };}); - - /* - * Test queuing: publishing a series of messages that start before the lib is connected - * Also checks they arrive in the right order - */ - testOnAllTransports(exports, 'publishQueued', function(realtimeOpts) { return function(test) { - test.expect(150); - var txRealtime, rxRealtime; - try { - txRealtime = helper.AblyRealtime(utils.mixin(realtimeOpts, { autoConnect: false })); - rxRealtime = helper.AblyRealtime(); - var txChannel = txRealtime.channels.get('publishQueued_' + String(Math.random()).substr(2)); - var rxChannel = rxRealtime.channels.get(txChannel.name); - - async.series([ - function(cb) { - rxRealtime.connection.once('connected', function() { cb(); }); - }, - function(cb) { - rxChannel.attach(function(err) { cb(err); }); - }, - function(cb) { - async.parallel([ - function(parCb) { - var expectedMsgNum = 0; - rxChannel.subscribe('event', function(msg) { - var num = msg.data.num; - test.ok(true, 'Received event ' + num); - test.equal(expectedMsgNum, num, 'Event ' + num + ' was in the right order'); - expectedMsgNum++; - if(num === 49) parCb(); + ); }); - }, - function(parCb) { - var ackd = 0; - var publish = function(i) { - txChannel.publish('event', {num: i}, function(err) { - test.ok(!err, 'successfully published ' + i + (err ? ' err was ' + displayError(err) : '')); - ackd++; - if(ackd === 50) parCb(); - }); - if(i < 49) { - setTimeout(function() { - publish(i + 1); - }, 20); + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }; + }); + + /* + * Test queuing: publishing a series of messages that start before the lib is connected + * Also checks they arrive in the right order + */ + testOnAllTransports('publishQueued', function (realtimeOpts) { + return function (done) { + var txRealtime, rxRealtime; + try { + txRealtime = helper.AblyRealtime(utils.mixin(realtimeOpts, { autoConnect: false })); + rxRealtime = helper.AblyRealtime(); + var txChannel = txRealtime.channels.get('publishQueued_' + String(Math.random()).substr(2)); + var rxChannel = rxRealtime.channels.get(txChannel.name); + + async.series( + [ + function (cb) { + rxRealtime.connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + rxChannel.attach(function (err) { + cb(err); + }); + }, + function (cb) { + async.parallel( + [ + function (parCb) { + var expectedMsgNum = 0; + rxChannel.subscribe('event', function (msg) { + var num = msg.data.num; + try { + expect(expectedMsgNum).to.equal(num, 'Event ' + num + ' was in the right order'); + } catch (err) { + parCb(err); + return; + } + expectedMsgNum++; + if (num === 49) parCb(); + }); + }, + function (parCb) { + var ackd = 0; + var publish = function (i) { + txChannel.publish('event', { num: i }, function (err) { + try { + expect( + !err, + 'successfully published ' + i + (err ? ' err was ' + displayError(err) : '') + ).to.be.ok; + } catch (err) { + parCb(err); + return; + } + ackd++; + if (ackd === 50) parCb(); + }); + if (i < 49) { + setTimeout(function () { + publish(i + 1); + }, 20); + } + }; + publish(0); + }, + function (parCb) { + txRealtime.connection.once('connected', function () { + parCb(); + }); + txRealtime.connection.connect(); + } + ], + cb + ); } - }; - publish(0); - }, - function(parCb) { - txRealtime.connection.once('connected', function() { parCb(); }); - txRealtime.connection.connect(); + ], + function (err) { + closeAndFinish(done, [rxRealtime, txRealtime], err); + } + ); + } catch (err) { + closeAndFinish(done, [rxRealtime, txRealtime], err); + } + }; + }); + + /* + * Test that a message is not sent back to the same realtime client + * when echoMessages is false (RTC1a and RTL7f) + * + * Test that a message is sent back to the same realtime client + * when echoMessages is true (RTC1a and RTL7f) + */ + it('publishEcho', function (done) { + // set up two realtimes + var rtNoEcho = helper.AblyRealtime({ echoMessages: false }), + rtEcho = helper.AblyRealtime({ echoMessages: true }), + rtNoEchoChannel = rtNoEcho.channels.get('publishecho'), + rtEchoChannel = rtEcho.channels.get('publishecho'), + testMsg1 = 'Hello', + testMsg2 = 'World!'; + + // We expect to see testMsg2 on rtNoEcho and testMsg1 on both rtNoEcho and rtEcho + var receivedMessagesNoEcho = [], + receivedMessagesEcho = []; + + var finishTest = function () { + if (receivedMessagesNoEcho.length + receivedMessagesEcho.length == 3) { + try { + expect(receivedMessagesNoEcho.length).to.equal(1, 'Received exactly one message on rtNoEcho'); + expect(receivedMessagesEcho.length).to.equal(2, 'Received exactly two messages on rtEcho'); + expect(receivedMessagesNoEcho[0]).to.equal(testMsg2, 'Received testMsg2 on rtNoEcho'); + expect(receivedMessagesEcho[0]).to.equal(testMsg1, 'Received testMsg1 on rtEcho first'); + expect(receivedMessagesEcho[1]).to.equal(testMsg2, 'Received testMsg2 on rtEcho second'); + } catch (err) { + closeAndFinish(done, [rtNoEcho, rtEcho], err); + return; } - ], cb); - } - ], function() { - closeAndFinish(test, [rxRealtime, txRealtime]); - }); - } catch(e) { - test.ok(false, 'test failed with exception: ' + e.stack); - closeAndFinish(test, [rxRealtime, txRealtime]); - } - };}); - - /* - * Test that a message is not sent back to the same realtime client - * when echoMessages is false (RTC1a and RTL7f) - * - * Test that a message is sent back to the same realtime client - * when echoMessages is true (RTC1a and RTL7f) - */ - exports.publishEcho = function(test) { - test.expect(7); - - // set up two realtimes - var rtNoEcho = helper.AblyRealtime({ echoMessages: false }), - rtEcho = helper.AblyRealtime({ echoMessages: true }), - - rtNoEchoChannel = rtNoEcho.channels.get('publishecho'), - rtEchoChannel = rtEcho.channels.get('publishecho'), - - testMsg1 = 'Hello', - testMsg2 = 'World!'; - - // We expect to see testMsg2 on rtNoEcho and testMsg1 on both rtNoEcho and rtEcho - var receivedMessagesNoEcho = [], - receivedMessagesEcho = []; - - var finishTest = function() { - if (receivedMessagesNoEcho.length + receivedMessagesEcho.length == 3) { - test.equal(receivedMessagesNoEcho.length, 1, 'Received exactly one message on rtNoEcho'); - test.equal(receivedMessagesEcho.length, 2, 'Received exactly two messages on rtEcho'); + closeAndFinish(done, [rtNoEcho, rtEcho]); + } + }; + + // attach rtNoEchoChannel + rtNoEchoChannel.attach(function (err) { try { - test.equal(receivedMessagesNoEcho[0], testMsg2, 'Received testMsg2 on rtNoEcho'); - test.equal(receivedMessagesEcho[0], testMsg1, 'Received testMsg1 on rtEcho first'); - test.equal(receivedMessagesEcho[1], testMsg2, 'Received testMsg2 on rtEcho second'); - } catch(e) { - test.ok(false, 'Failed to find an expected message in the received messages'); + expect(!err, 'Attached to rtNoEchoChannel with no error').to.be.ok; + } catch (err) { + closeAndFinish(done, [rtNoEcho, rtEcho], err); + return; } - closeAndFinish(test, [rtNoEcho, rtEcho]); - } - } + monitorConnection(done, rtNoEcho); - // attach rtNoEchoChannel - rtNoEchoChannel.attach(function(err) { - test.ok(!err,'Attached to rtNoEchoChannel with no error'); - monitorConnection(test, rtNoEcho); + // once rtNoEchoChannel attached, subscribe to event0 + rtNoEchoChannel.subscribe('event0', function (msg) { + receivedMessagesNoEcho.push(msg.data); + finishTest(); + }); - // once rtNoEchoChannel attached, subscribe to event0 - rtNoEchoChannel.subscribe('event0', function(msg) { - receivedMessagesNoEcho.push(msg.data); - finishTest(); - }); + // attach rtEchoChannel + rtEchoChannel.attach(function (err) { + try { + expect(!err, 'Attached to rtEchoChannel with no error').to.be.ok; + } catch (err) { + closeAndFinish(done, [rtNoEcho, rtEcho], err); + return; + } + monitorConnection(done, rtEcho); - // attach rtEchoChannel - rtEchoChannel.attach(function(err) { - test.ok(!err,'Attached to rtEchoChannel with no error'); - monitorConnection(test, rtEcho); + // once rtEchoChannel attached, subscribe to event0 + rtEchoChannel.subscribe('event0', function (msg) { + receivedMessagesEcho.push(msg.data); + finishTest(); + }); - // once rtEchoChannel attached, subscribe to event0 - rtEchoChannel.subscribe('event0', function(msg) { - receivedMessagesEcho.push(msg.data); - finishTest(); - }); - - // publish testMsg1 via rtNoEcho - rtNoEchoChannel.publish('event0', testMsg1, function() { - // publish testMsg2 via rtEcho - rtEchoChannel.publish('event0', testMsg2); + // publish testMsg1 via rtNoEcho + rtNoEchoChannel.publish('event0', testMsg1, function () { + // publish testMsg2 via rtEcho + rtEchoChannel.publish('event0', testMsg2); + }); }); - }); }); - }; - exports.publishVariations = function(test) { - var testData = 'Some data'; - var testArguments = [ - [{name: 'objectWithName'}], - [{name: 'objectWithNameAndNullData', data: null}], - [{name: 'objectWithNameAndUndefinedData', data: undefined}], - [{name: 'objectWithNameAndEmptyStringData', data: ''}], - ['nameAndNullData', null], - ['nameAndUndefinedData', undefined], - ['nameAndEmptyStringData', ''], - ['nameAndData', testData], - [{name: 'objectWithNameAndData', data: testData}], - // 5 messages with null name, - [null, testData], - [{name: null, data: testData}], - [null, null], - [{name: null}], - [{name: null, data: null}] - ]; - var realtime; - - test.expect(testArguments.length * 2); - try { - /* set up realtime */ - realtime = helper.AblyRealtime(); - var rest = helper.AblyRest(); + it('publishVariations', function (done) { + var testData = 'Some data'; + var testArguments = [ + [{ name: 'objectWithName' }], + [{ name: 'objectWithNameAndNullData', data: null }], + [{ name: 'objectWithNameAndUndefinedData', data: undefined }], + [{ name: 'objectWithNameAndEmptyStringData', data: '' }], + ['nameAndNullData', null], + ['nameAndUndefinedData', undefined], + ['nameAndEmptyStringData', ''], + ['nameAndData', testData], + [{ name: 'objectWithNameAndData', data: testData }], + // 5 messages with null name, + [null, testData], + [{ name: null, data: testData }], + [null, null], + [{ name: null }], + [{ name: null, data: null }] + ]; + var realtime; + + try { + /* set up realtime */ + realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + + /* connect and attach */ + realtime.connection.on('connected', function () { + var rtChannel = realtime.channels.get('publishVariations'); + rtChannel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - /* connect and attach */ - realtime.connection.on('connected', function() { - var rtChannel = realtime.channels.get('publishVariations'); - rtChannel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - return; - } + /* subscribe to different message types */ + var messagesReceived = 0; + rtChannel.subscribe(function (msg) { + ++messagesReceived; + try { + switch (msg.name) { + case 'objectWithName': + case 'objectWithNameAndCallback': + case 'objectWithNameAndNullData': + case 'objectWithNameAndUndefinedData': + case 'nameAndNullData': + case 'nameAndUndefinedData': + expect(typeof msg.data).to.equal('undefined', 'Msg data was received where none expected'); + break; + case 'nameAndEmptyStringData': + case 'objectWithNameAndEmptyStringData': + expect(msg.data).to.equal( + '', + 'Msg data received was a ' + typeof msg.data + ' when should have been an empty string' + ); + break; + case 'objectWithNameAndFalseData': + case 'nameAndFalseData': + expect(msg.data).to.equal( + false, + 'Msg data received was a ' + typeof msg.data + ' when should have been a bool false' + ); + break; + case 'nameAndData': + case 'nameAndDataAndCallback': + case 'objectWithNameAndData': + case 'objectWithNameAndDataAndCallback': + expect(msg.data).to.equal(testData, 'Msg data ' + msg.data + 'Unexpected message data received'); + break; + case undefined: + if (msg.data) { + // 3 messages: null name and data, null name and data and callback, object with null name and data + expect(msg.data).to.equal(testData, 'Msg data ' + msg.data + 'Unexpected message data received'); + } else { + // 3 messages: null name and null data, object with null name and no data, object with null name and null data + expect(typeof msg.data).to.equal('undefined', 'Msg data was received where none expected'); + } + break; + default: + closeAndFinish(done, realtime, new Error('Unexpected message ' + msg.name + 'received')); + return; + } + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } - /* subscribe to different message types */ - var messagesReceived = 0; - rtChannel.subscribe(function(msg) { - test.ok(true, 'Received ' + msg.name); - ++messagesReceived; - switch(msg.name) { - case 'objectWithName': - case 'objectWithNameAndCallback': - case 'objectWithNameAndNullData': - case 'objectWithNameAndUndefinedData': - case 'nameAndNullData': - case 'nameAndUndefinedData': - test.equal(typeof(msg.data), 'undefined', 'Msg data was received where none expected'); - break; - case 'nameAndEmptyStringData': - case 'objectWithNameAndEmptyStringData': - test.equal(msg.data, '', 'Msg data received was a ' + typeof(msg.data) + ' when should have been an empty string'); - break; - case 'objectWithNameAndFalseData': - case 'nameAndFalseData': - test.equal(msg.data, false, 'Msg data received was a ' + typeof(msg.data) + ' when should have been a bool false'); - break; - case 'nameAndData': - case 'nameAndDataAndCallback': - case 'objectWithNameAndData': - case 'objectWithNameAndDataAndCallback': - test.equal(msg.data, testData, 'Msg data ' + msg.data + 'Unexpected message data received'); - break; - case undefined: - if (msg.data) { - // 3 messages: null name and data, null name and data and callback, object with null name and data - test.equal(msg.data, testData, 'Msg data ' + msg.data + 'Unexpected message data received'); - } else { - // 3 messages: null name and null data, object with null name and no data, object with null name and null data - test.equal(typeof(msg.data), 'undefined', 'Msg data was received where none expected'); + if (messagesReceived == testArguments.length) { + setTimeout(function () { + closeAndFinish(done, realtime); + }, 2000); + } + }); + + /* publish events */ + var restChannel = rest.channels.get('publishVariations'); + async.eachSeries( + testArguments, + function iterator(args, callback) { + args.push(callback); + restChannel.publish.apply(restChannel, args); + }, + function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - break; - default: - test.ok(false, 'Unexpected message ' + msg.name + 'received'); - closeAndFinish(test, realtime); + } + ); + }); + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + it('publishDisallowed', function (done) { + var testArguments = [ + [{ name: 'objectAndBoolData', data: false }], + ['nameAndBoolData', false], + [{ name: 'objectAndNumericData', data: 0 }], + ['nameAndNumericData', 0], + [{ name: 'objectAndOtherObjectData', data: new Date() }], + ['nameAndOtherObjectData', new Date()] + ]; + + try { + /* set up realtime */ + var realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + + /* connect and attach */ + realtime.connection.on('connected', function () { + var rtChannel = realtime.channels.get('publishDisallowed'); + rtChannel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - if (messagesReceived == testArguments.length) { - setTimeout(function() { - closeAndFinish(test, realtime); - }, 2000); + /* publish events */ + var restChannel = rest.channels.get('publishDisallowed'); + for (var i = 0; i < testArguments.length; i++) { + try { + restChannel.publish.apply(restChannel, testArguments[i]); + closeAndFinish(done, realtime, new Error('Exception was not raised')); + } catch (err) { + try { + expect(err.code).to.equal(40013, 'Invalid data type exception raised'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + } } + closeAndFinish(done, realtime); }); + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - /* publish events */ - var restChannel = rest.channels.get('publishVariations'); - async.eachSeries(testArguments, function iterator(args, callback) { - args.push(callback); - restChannel.publish.apply(restChannel, args); - }, function(err) { - if(err) { - test.ok(false, 'Error received by publish callback ' + displayError(err)); - closeAndFinish(test, realtime); + it('publishEncodings', function (done) { + var testData = 'testData'; + var testArguments = [ + // valid + [{ name: 'justJson', encoding: 'json', data: '{"foo":"bar"}' }], + // invalid -- encoding ending in utf-8 implies data is binary + [{ name: 'jsonUtf8string', encoding: 'json/utf-8', data: '{"foo":"bar"}' }], + // valid + [{ name: 'utf8base64', encoding: 'utf-8/base64', data: 'dGVzdERhdGE=' }], + // invalid -- nonsense/corrupt encoding + [{ name: 'nonsense', encoding: 'choahofhpxf', data: testData }] + ]; + + try { + /* set up realtime */ + var realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + + /* connect and attach */ + realtime.connection.on('connected', function () { + var rtChannel = realtime.channels.get('publishEncodings'); + rtChannel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); return; } - }); - }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - exports.publishDisallowed = function(test) { - var testArguments = [ - [{name: 'objectAndBoolData', data: false}], - ['nameAndBoolData', false], - [{name: 'objectAndNumericData', data: 0}], - ['nameAndNumericData', 0], - [{name: 'objectAndOtherObjectData', data: new Date()}], - ['nameAndOtherObjectData', new Date()] - ]; - - test.expect(testArguments.length * 2); - try { - /* set up realtime */ - var realtime = helper.AblyRealtime(); - var rest = helper.AblyRest(); + var subscribefn = function (cb) { + var messagesReceived = 0; + rtChannel.subscribe(function (msg) { + ++messagesReceived; + try { + switch (msg.name) { + case 'justJson': + expect(msg.data).to.deep.equal({ foo: 'bar' }, 'justJson: correct decoded data'); + expect(msg.encoding).to.deep.equal(null, 'justJson: encoding stripped on decoding'); + break; + case 'jsonUtf8string': + expect(msg.data).to.equal('{"foo":"bar"}', 'justJsonUTF8string: data should be untouched'); + expect(msg.encoding).to.equal('json/utf-8', 'justJsonUTF8string: encoding should be untouched'); + break; + case 'utf8base64': + expect(msg.data).to.equal('testData', 'utf8base64: correct decoded data'); + expect(msg.encoding).to.equal(null, 'utf8base64: encoding stripped on decoding'); + break; + case 'nonsense': + expect(msg.data).to.deep.equal(testData, 'nonsense: data untouched'); + expect(msg.encoding).to.equal('choahofhpxf', 'nonsense: encoding untouched'); + break; + default: + cb(new Error('Unexpected message ' + msg.name + ' received')); + } + if (messagesReceived == testArguments.length) { + cb(); + } + } catch (err) { + cb(err); + } + }); + }; - /* connect and attach */ - realtime.connection.on('connected', function() { - var rtChannel = realtime.channels.get('publishDisallowed'); - rtChannel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - return; - } + /* publish events */ + var publishfn = function (cb) { + var restChannel = rest.channels.get('publishEncodings'); + async.eachSeries( + testArguments, + function iterator(item, callback) { + try { + restChannel.publish(item, function (err) { + try { + expect(!err, 'Successfully published').to.be.ok; + } catch (err) { + callback(err); + } + callback(); + }); + } catch (err) { + callback(err); + } + }, + cb + ); + }; - /* publish events */ - var restChannel = rest.channels.get('publishDisallowed'); - for(var i = 0; i < testArguments.length; i++) { - try { - restChannel.publish.apply(restChannel, testArguments[i]); - test.ok(false, "Exception was not raised"); - } catch (e) { - test.ok(true, "Exception correctly raised"); - test.equal(e.code, 40013, "Invalid data type exception raised"); - } - } - closeAndFinish(test, realtime); + async.parallel([subscribefn, publishfn], function (err) { + closeAndFinish(done, realtime, err); + }); + }); }); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); - exports.publishEncodings = function(test) { - var testData = 'testData'; - var testArguments = [ - // valid - [{name: 'justJson', encoding: 'json', data: '{\"foo\":\"bar\"}'}], - // invalid -- encoding ending in utf-8 implies data is binary - [{name: 'jsonUtf8string', encoding: 'json/utf-8', data: '{\"foo\":\"bar\"}'}], - // valid - [{name: 'utf8base64', encoding: 'utf-8/base64', data: 'dGVzdERhdGE='}], - // invalid -- nonsense/corrupt encoding - [{name: 'nonsense', encoding: 'choahofhpxf', data: testData}] - ]; - - test.expect(testArguments.length * 4); // One for sending, one for receiving, one each for data & encoding - try { - /* set up realtime */ - var realtime = helper.AblyRealtime(); + it('restpublish', function (done) { + var count = 10; var rest = helper.AblyRest(); + var realtime = helper.AblyRealtime(); + var messagesSent = []; + var sendchannel = rest.channels.get('restpublish'); + var recvchannel = realtime.channels.get('restpublish'); + /* subscribe to event */ + recvchannel.subscribe('event0', function (msg) { + try { + expect(-1).to.not.equal(utils.arrIndexOf(messagesSent, msg.data), 'Received unexpected message text'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + if (!--count) { + clearInterval(timer); + closeAndFinish(done, realtime); + } + }); + var timer = setInterval(function () { + // console.log('sending: ' + count); + var msgText = 'Hello world at: ' + new Date(); + messagesSent.push(msgText); + sendchannel.publish('event0', msgText); + }, 500); + }); - /* connect and attach */ - realtime.connection.on('connected', function() { - var rtChannel = realtime.channels.get('publishEncodings'); - rtChannel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); - return; + testOnAllTransports('publish', function (realtimeOpts) { + return function (done) { + var count = 10; + var cbCount = 10; + var checkFinish = function () { + if (count <= 0 && cbCount <= 0) { + closeAndFinish(done, realtime); } - - var subscribefn = function(cb) { - var messagesReceived = 0; - rtChannel.subscribe(function(msg) { - test.ok(true, 'Received ' + msg.name); - ++messagesReceived; - switch(msg.name) { - case 'justJson': - test.deepEqual(msg.data, {foo: "bar"}, 'justJson: correct decoded data'); - test.equal(msg.encoding, null, 'justJson: encoding stripped on decoding'); - break; - case 'jsonUtf8string': - test.equal(msg.data, '{\"foo\":\"bar\"}', 'justJsonUTF8string: data should be untouched'); - test.equal(msg.encoding, 'json/utf-8', 'justJsonUTF8string: encoding should be untouched'); - break; - case 'utf8base64': - test.equal(msg.data, "testData", 'utf8base64: correct decoded data'); - test.equal(msg.encoding, null, 'utf8base64: encoding stripped on decoding'); - break; - case 'nonsense': - test.deepEqual(msg.data, testData, 'nonsense: data untouched'); - test.equal(msg.encoding, 'choahofhpxf', 'nonsense: encoding untouched'); - break; - default: - test.ok(false, 'Unexpected message ' + msg.name + ' received'); - cb(); - } - if (messagesReceived == testArguments.length) { - cb(); - } - }); + }; + var onPublish = function () { + --cbCount; + checkFinish(); + }; + var realtime = helper.AblyRealtime(realtimeOpts); + var channel = realtime.channels.get('publish ' + JSON.stringify(realtimeOpts)); + /* subscribe to event */ + channel.subscribe( + 'event0', + function () { + --count; + checkFinish(); + }, + function () { + var dataFn = function () { + return 'Hello world at: ' + new Date(); + }; + publishAtIntervals(count, channel, dataFn, onPublish); } + ); + }; + }); - /* publish events */ - var publishfn = function(cb) { - var restChannel = rest.channels.get('publishEncodings'); - async.eachSeries(testArguments, function iterator(item, callback) { - try { - restChannel.publish(item, function(err) { - test.ok(!err, "Successfully published"); - callback(err); - }); - } catch (e) { - test.ok(false, "Failed to publish"); - } - }, cb); - } + it('duplicateMsgId', function (done) { + var realtime = helper.AblyRealtime({ log: { level: 4 } }), + channel = realtime.channels.get('duplicateMsgId'), + received = 0; - async.parallel([subscribefn, publishfn], function() { - closeAndFinish(test, realtime); - }) - }); + channel.subscribe(function (_msg) { + received++; }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + channel.once('attached', function () { + realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 15, + channel: channel.name, + id: 'foo:0', + connectionSerial: 0, + messages: [{ name: null, data: null }] + }) + ); + /* add some nonmessage channel message inbetween */ + realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 11, + channel: channel.name + }) + ); + + realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 15, + channel: channel.name, + id: 'foo:0', + connectionSerial: 1, + messages: [{ name: null, data: null }] + }) + ); - exports.restpublish = function(test) { - var count = 10; - var rest = helper.AblyRest(); - var realtime = helper.AblyRealtime(); - test.expect(2 * count); - var messagesSent = []; - var sendchannel = rest.channels.get('restpublish'); - var recvchannel = realtime.channels.get('restpublish'); - /* subscribe to event */ - recvchannel.subscribe('event0', function(msg) { - test.ok(true, 'Received event0'); - test.notEqual(-1, utils.arrIndexOf(messagesSent, msg.data), 'Received unexpected message text'); - if(!--count) { - clearInterval(timer); - closeAndFinish(test, realtime); - } + try { + expect(received).to.equal(1); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); }); - var timer = setInterval(function() { - // console.log('sending: ' + count); - var msgText = 'Hello world at: ' + new Date(); - messagesSent.push(msgText); - sendchannel.publish('event0', msgText); - }, 500); - }; - testOnAllTransports(exports, 'publish', function(realtimeOpts) { return function(test) { - var count = 10; - var cbCount = 10; - var checkFinish = function() { - if(count <= 0 && cbCount <= 0) { - closeAndFinish(test, realtime); - } - }; - var onPublish = function() { - --cbCount; - checkFinish(); - }; - var realtime = helper.AblyRealtime(realtimeOpts); - test.expect(count); - var channel = realtime.channels.get('publish ' + JSON.stringify(realtimeOpts)); - /* subscribe to event */ - channel.subscribe('event0', function() { - test.ok(true, 'Received event0'); - --count; - checkFinish(); - }, function() { - var dataFn = function() { return 'Hello world at: ' + new Date(); }; - publishAtIntervals(count, channel, dataFn, onPublish); - }); - }}); - - exports.duplicateMsgId = function(test) { - test.expect(1); - var realtime = helper.AblyRealtime({log: {level: 4}}), - connectionManager = realtime.connection.connectionManager, - channel = realtime.channels.get('duplicateMsgId'), - received = 0; - - channel.subscribe(function(_msg) { - received++; - }) - channel.once('attached', function() { - realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 15, - channel: channel.name, - id: "foo:0", - connectionSerial: 0, - messages: [{name: null, data: null}] - })); - - /* add some nonmessage channel message inbetween */ - realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 11, - channel: channel.name - })); - - realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 15, - channel: channel.name, - id: "foo:0", - connectionSerial: 1, - messages: [{name: null, data: null}] - })); - - test.equal(received, 1); - closeAndFinish(test, realtime); - }); - }; + it('duplicateConnectionId', function (done) { + var realtime = helper.AblyRealtime({ log: { level: 4 } }), + channel = realtime.channels.get('duplicateConnectionId'), + received = 0; - exports.duplicateConnectionId = function(test) { - test.expect(1); - var realtime = helper.AblyRealtime({log: {level: 4}}), - connectionManager = realtime.connection.connectionManager, - channel = realtime.channels.get('duplicateConnectionId'), - received = 0; + channel.subscribe(function (_msg) { + received++; + }); + channel.once('attached', function () { + realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 15, + channel: channel.name, + id: 'foo:0', + connectionSerial: 0, + messages: [{ name: null, data: null }] + }) + ); + + realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 15, + channel: channel.name, + id: 'bar:0', + connectionSerial: 0, + messages: [{ name: null, data: null }] + }) + ); - channel.subscribe(function(_msg) { - received++; - }); - channel.once('attached', function() { - realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 15, - channel: channel.name, - id: "foo:0", - connectionSerial: 0, - messages: [{name: null, data: null}] - })); - - realtime.connection.connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 15, - channel: channel.name, - id: "bar:0", - connectionSerial: 0, - messages: [{name: null, data: null}] - })); - - test.equal(received, 1); - closeAndFinish(test, realtime); + try { + expect(received).to.equal(1); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); }); - }; - /* Authenticate with a clientId and ensure that the clientId is not sent in the Message + /* Authenticate with a clientId and ensure that the clientId is not sent in the Message and is implicitly added when published */ - exports.implicit_client_id_0 = function(test) { - var clientId = 'implicit_client_id_0', + it('implicit_client_id_0', function (done) { + var clientId = 'implicit_client_id_0', realtime = helper.AblyRealtime({ clientId: clientId }); - test.expect(3); - - realtime.connection.once('connected', function() { - var transport = realtime.connection.connectionManager.activeProtocol.transport, + realtime.connection.once('connected', function () { + var transport = realtime.connection.connectionManager.activeProtocol.transport, originalSend = transport.send; - transport.send = function(message) { - if (message.action === 15) { - test.ok(message.messages[0].name === 'event0', 'Outgoing message interecepted'); - test.ok(!message.messages[0].clientId, 'client ID is not added by the client library as it is implicit'); - } - originalSend.apply(transport, arguments); - }; + transport.send = function (message) { + try { + if (message.action === 15) { + expect(message.messages[0].name === 'event0', 'Outgoing message interecepted').to.be.ok; + expect(!message.messages[0].clientId, 'client ID is not added by the client library as it is implicit').to + .be.ok; + } + originalSend.apply(transport, arguments); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }; - var channel = realtime.channels.get('implicit_client_id_0'); - /* subscribe to event */ - channel.subscribe('event0', function(message) { - test.ok(message.clientId == clientId, 'Client ID was added implicitly'); - closeAndFinish(test, realtime); + var channel = realtime.channels.get('implicit_client_id_0'); + /* subscribe to event */ + channel.subscribe('event0', function (message) { + try { + expect(message.clientId == clientId, 'Client ID was added implicitly').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + returnl; + } + closeAndFinish(done, realtime); + }); + channel.publish('event0', null); }); - channel.publish('event0', null); }); - }; - /* Authenticate with a clientId and explicitly provide the same clientId in the Message + /* Authenticate with a clientId and explicitly provide the same clientId in the Message and ensure it is published */ - exports.explicit_client_id_0 = function(test) { - var clientId = 'explicit_client_id_0', - /* Use a fixed transport as intercepting transport.send */ - realtime = helper.AblyRealtime({ clientId: clientId, transports: [helper.bestTransport] }); - - test.expect(4); + it('explicit_client_id_0', function (done) { + var clientId = 'explicit_client_id_0', + /* Use a fixed transport as intercepting transport.send */ + realtime = helper.AblyRealtime({ clientId: clientId, transports: [helper.bestTransport] }); - realtime.connection.once('connected', function() { - var transport = realtime.connection.connectionManager.activeProtocol.transport, + realtime.connection.once('connected', function () { + var transport = realtime.connection.connectionManager.activeProtocol.transport, originalSend = transport.send; - transport.send = function(message) { - if (message.action === 15) { - test.ok(message.messages[0].name === 'event0', 'Outgoing message interecepted'); - test.ok(message.messages[0].clientId === clientId, 'client ID is present when published to Ably'); - } - originalSend.apply(transport, arguments); - }; + transport.send = function (message) { + try { + if (message.action === 15) { + expect(message.messages[0].name === 'event0', 'Outgoing message interecepted').to.be.ok; + expect(message.messages[0].clientId === clientId, 'client ID is present when published to Ably').to.be.ok; + } + originalSend.apply(transport, arguments); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }; - var channel = realtime.channels.get('explicit_client_id_0'); - /* subscribe to event */ - channel.attach(function(err) { - if(err) { - test.ok(!err, err && helper.displayError(err)); - closeAndFinish(test, realtime); - } - async.parallel([ - function(cb) { - channel.subscribe('event0', function(message) { - test.ok(message.clientId == clientId, 'Client ID was added implicitly'); - cb(); - }); - }, - function(cb) { - channel.publish({ name: 'event0', clientId: clientId }, function(err) { - cb(err); - }); + var channel = realtime.channels.get('explicit_client_id_0'); + /* subscribe to event */ + channel.attach(function (err) { + if (err) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); } - ], function(err) { - test.ok(!err, err && helper.displayError(err)); - closeAndFinish(test, realtime); + async.parallel( + [ + function (cb) { + channel.subscribe('event0', function (message) { + try { + expect(message.clientId == clientId, 'Client ID was added implicitly').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + }, + function (cb) { + channel.publish({ name: 'event0', clientId: clientId }, function (err) { + cb(err); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); }); }); }); - }; - - /* Authenticate with a clientId and explicitly provide a different invalid clientId in the Message + /* Authenticate with a clientId and explicitly provide a different invalid clientId in the Message and expect it to not be published and be rejected */ - exports.explicit_client_id_1 = function(test) { - var clientId = 'explicit_client_id_1', + it('explicit_client_id_1', function (done) { + var clientId = 'explicit_client_id_1', invalidClientId = 'invalid', rest = helper.AblyRest(); - test.expect(4); - - rest.auth.requestToken({ clientId: clientId }, function(err, token) { - test.ok(token.clientId === clientId, 'client ID is present in the Token'); + rest.auth.requestToken({ clientId: clientId }, function (err, token) { + if (err) { + done(err); + return; + } + try { + expect(token.clientId === clientId, 'client ID is present in the Token').to.be.ok; + } catch (err) { + done(err); + return; + } - /* Use a fixed transport as intercepting transport.send */ - var realtime = helper.AblyRealtime({ token: token.token, transports: [helper.bestTransport] }), + /* Use a fixed transport as intercepting transport.send */ + var realtime = helper.AblyRealtime({ token: token.token, transports: [helper.bestTransport] }), channel = realtime.channels.get('explicit_client_id_1'); - // Publish before authentication to ensure the client library does not reject the message as the clientId is not known - channel.publish({ name: 'event0', clientId: invalidClientId }, function(err) { - test.ok(err, 'Message was not published'); - setTimeout(function() { closeAndFinish(test, realtime); }, 500); // ensure that the message is not published - }); + // Publish before authentication to ensure the client library does not reject the message as the clientId is not known + channel.publish({ name: 'event0', clientId: invalidClientId }, function (err) { + try { + expect(err, 'Message was not published').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + } + setTimeout(function () { + closeAndFinish(done, realtime); + }, 500); // ensure that the message is not published + }); - realtime.connection.connectionManager.on('transport.pending', function(transport) { - var originalSend = transport.send; + realtime.connection.connectionManager.on('transport.pending', function (transport) { + var originalSend = transport.send; - transport.send = function(message) { - if (message.action === 15) { - test.ok(message.messages[0].name === 'event0', 'Outgoing message interecepted'); - test.ok(message.messages[0].clientId === invalidClientId, 'client ID is present when published to Ably'); - } - originalSend.apply(transport, arguments); - }; + transport.send = function (message) { + try { + if (message.action === 15) { + expect(message.messages[0].name === 'event0', 'Outgoing message interecepted').to.be.ok; + expect(message.messages[0].clientId === invalidClientId, 'client ID is present when published to Ably') + .to.be.ok; + } + originalSend.apply(transport, arguments); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }; - /* subscribe to event */ - channel.subscribe('event0', function(message) { - test.ok(false, 'Message should never have been received'); + /* subscribe to event */ + channel.subscribe('event0', function (message) { + closeAndFinish(done, realtime, new Error('Message should never have been received')); + }); }); }); }); - }; - exports.subscribe_with_event_array = function(test) { - var realtime = helper.AblyRealtime(), - channel = realtime.channels.get('subscribe_with_event_array'); - - async.series([ - function(cb) { - realtime.connection.once('connected', function() { cb(); }); - }, - function(cb) { - channel.attach(function(err) { cb(err); }); - }, - function(outercb) { - async.parallel([ - function(innercb) { - var received = 0; - channel.subscribe(['a', 'b'], function(message) { - test.ok(message.name === 'a' || message.name === 'b', 'Correct messages received'); - ++received; - if(received === 2) { - /* wait a tick to make sure no more messages come in */ - utils.nextTick(function() { innercb(); }); - } + it('subscribe_with_event_array', function (done) { + var realtime = helper.AblyRealtime(), + channel = realtime.channels.get('subscribe_with_event_array'); + + async.series( + [ + function (cb) { + realtime.connection.once('connected', function () { + cb(); }); }, - function(innercb) { - channel.publish([{name: 'a'}, {name: 'b'}, {name: 'c'}, {name: 'd'}], function(err) { - innercb(err); + function (cb) { + channel.attach(function (err) { + cb(err); }); + }, + function (outercb) { + async.parallel( + [ + function (innercb) { + var received = 0; + channel.subscribe(['a', 'b'], function (message) { + try { + expect(message.name === 'a' || message.name === 'b', 'Correct messages received').to.be.ok; + } catch (err) { + innercb(err); + return; + } + ++received; + if (received === 2) { + /* wait a tick to make sure no more messages come in */ + utils.nextTick(function () { + innercb(); + }); + } + }); + }, + function (innercb) { + channel.publish([{ name: 'a' }, { name: 'b' }, { name: 'c' }, { name: 'd' }], function (err) { + innercb(err); + }); + } + ], + outercb + ); } - ], outercb) - }], function(err) { - test.ok(!err, err && helper.displayError(err)); - closeAndFinish(test, realtime); - }); - }; + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }); + + it('extras_field', function (done) { + var realtime = helper.AblyRealtime(), + channel = realtime.channels.get('extras_field'), + extras = { headers: { some: 'metadata' } }; - exports.extras_field = function(test) { - var realtime = helper.AblyRealtime(), - channel = realtime.channels.get('extras_field'), - extras = {headers: {some: 'metadata'}}; - - test.expect(3); - - async.series([ - function(cb) { - realtime.connection.once('connected', function() { cb(); }); - }, - channel.attach.bind(channel), - function(outercb) { - async.parallel([ - function(innercb) { - var received = 0; - channel.subscribe(function(message) { - test.ok(true, 'Message received'); - test.deepEqual(message.extras, extras, 'Check extras is present'); - innercb(); + async.series( + [ + function (cb) { + realtime.connection.once('connected', function () { + cb(); }); }, - function(innercb) { - channel.publish([{name: 'a', extras: extras}], innercb); + channel.attach.bind(channel), + function (outercb) { + async.parallel( + [ + function (innercb) { + var received = 0; + channel.subscribe(function (message) { + try { + expect(message.extras).to.deep.equal(extras, 'Check extras is present'); + } catch (err) { + innercb(err); + return; + } + innercb(); + }); + }, + function (innercb) { + channel.publish([{ name: 'a', extras: extras }], innercb); + } + ], + outercb + ); } - ], outercb) - }], function(err) { - test.ok(!err, err && helper.displayError(err)); - closeAndFinish(test, realtime); - }); - }; + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }); + + /* TO3l8; CD2C; RSL1i */ + it('maxMessageSize', function (done) { + var realtime = helper.AblyRealtime(), + connectionManager = realtime.connection.connectionManager, + channel = realtime.channels.get('maxMessageSize'); - /* TO3l8; CD2C; RSL1i */ - exports.maxMessageSize = function(test) { - test.expect(2); - var realtime = helper.AblyRealtime(), - connectionManager = realtime.connection.connectionManager, - channel = realtime.channels.get('maxMessageSize'); - - realtime.connection.once('connected', function() { - connectionManager.once('connectiondetails', function(details) { - channel.publish('foo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', function(err) { - test.ok(err, 'Check publish refused'); - test.equal(err.code, 40009); - closeAndFinish(test, realtime); + realtime.connection.once('connected', function () { + connectionManager.once('connectiondetails', function (details) { + channel.publish('foo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', function (err) { + try { + expect(err, 'Check publish refused').to.be.ok; + expect(err.code).to.equal(40009); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); }); + var connectionDetails = connectionManager.connectionDetails; + connectionDetails.maxMessageSize = 64; + connectionManager.activeProtocol.getTransport().onProtocolMessage( + createPM({ + action: 4, + connectionDetails: connectionDetails + }) + ); }); - var connectionDetails = connectionManager.connectionDetails; - connectionDetails.maxMessageSize = 64; - connectionManager.activeProtocol.getTransport().onProtocolMessage(createPM({ - action: 4, - connectionDetails: connectionDetails - })); }); - }; - /* RTL6d: publish a series of messages that exercise various bundling - * constraints, check they're satisfied */ - exports.bundling = function(test) { - test.expect(30); - var realtime = helper.AblyRealtime({maxMessageSize: 256, autoConnect: false}), - channelOne = realtime.channels.get('bundlingOne'), - channelTwo = realtime.channels.get('bundlingTwo'); - - /* RTL6d3; RTL6d5 */ - channelTwo.publish('2a', {expectedBundle: 0}); - channelOne.publish('a', {expectedBundle: 1}); - channelOne.publish([{name: 'b', data: {expectedBundle: 1}}, {name: 'c', data: {expectedBundle: 1}}]); - channelOne.publish('d', {expectedBundle: 1}); - channelTwo.publish('2b', {expectedBundle: 2}); - channelOne.publish('e', {expectedBundle: 3}); - channelOne.publish({name: 'f', data: {expectedBundle: 3}}); - /* RTL6d2 */ - channelOne.publish({name: 'g', data: {expectedBundle: 4}, clientId: 'foo'}); - channelOne.publish({name: 'h', data: {expectedBundle: 4}, clientId: 'foo'}); - channelOne.publish({name: 'i', data: {expectedBundle: 5}, clientId: 'bar'}); - channelOne.publish('j', {expectedBundle: 6}); - /* RTL6d1 */ - channelOne.publish('k', {expectedBundle: 7, moreData: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}); - channelOne.publish('l', {expectedBundle: 8}); - /* RTL6d7 */ - channelOne.publish({name: 'm', id: 'bundle_m', data: {expectedBundle: 9}}); - channelOne.publish('z_last', {expectedBundle: 10}); - - var queue = realtime.connection.connectionManager.queuedMessages; - var messages; - for(var i=0; i<=10; i++) { - messages = queue.messages[i].message.messages || queue.messages[i].message.presence; - for(var j=0; j SUSPENDED -> ATTACHED, members map is preserved - * and only members that changed between ATTACHED states should result in - * presence events */ - exports.suspended_preserves_presence = function(test) { - test.expect(8); - var mainRealtime = helper.AblyRealtime({clientId: 'main', log: {level: 4}}), - continuousRealtime = helper.AblyRealtime({clientId: 'continuous', log: {level: 4}}), - leavesRealtime = helper.AblyRealtime({clientId: 'leaves', log: {level: 4}}), - channelName = 'suspended_preserves_presence', - mainChannel = mainRealtime.channels.get(channelName); - - monitorConnection(test, continuousRealtime); - monitorConnection(test, leavesRealtime); - var enter = function(rt) { - return function(outerCb) { - var channel = rt.channels.get(channelName); - async.series([ - function(cb) { rt.connection.whenState('connected', function() { cb(); }); }, - function(cb) { channel.attach(cb); }, - function(cb) { channel.presence.enter(cb); } - ], outerCb); + /* RTP5f; RTP11d + * Check that on ATTACHED -> SUSPENDED -> ATTACHED, members map is preserved + * and only members that changed between ATTACHED states should result in + * presence events */ + it('suspended_preserves_presence', function (done) { + var mainRealtime = helper.AblyRealtime({ clientId: 'main', log: { level: 4 } }), + continuousRealtime = helper.AblyRealtime({ clientId: 'continuous', log: { level: 4 } }), + leavesRealtime = helper.AblyRealtime({ clientId: 'leaves', log: { level: 4 } }), + channelName = 'suspended_preserves_presence', + mainChannel = mainRealtime.channels.get(channelName); + + monitorConnection(done, continuousRealtime); + monitorConnection(done, leavesRealtime); + var enter = function (rt) { + return function (outerCb) { + var channel = rt.channels.get(channelName); + async.series( + [ + function (cb) { + rt.connection.whenState('connected', function () { + cb(); + }); + }, + function (cb) { + channel.attach(cb); + }, + function (cb) { + channel.presence.enter(cb); + } + ], + outerCb + ); + }; }; - }; - var waitFor = function(expectedClientId) { - return function(cb) { - var presenceHandler = function(presmsg) { - if(expectedClientId == presmsg.clientId) { - mainChannel.presence.unsubscribe(presenceHandler); - cb(); - } + var waitFor = function (expectedClientId) { + return function (cb) { + var presenceHandler = function (presmsg) { + if (expectedClientId == presmsg.clientId) { + mainChannel.presence.unsubscribe(presenceHandler); + cb(); + } + }; + mainChannel.presence.subscribe(presenceHandler); }; - mainChannel.presence.subscribe(presenceHandler); }; - }; - async.series([ - enter(mainRealtime), - function(cb) { - async.parallel([ - waitFor('continuous'), - enter(continuousRealtime) - ], cb) - }, - function(cb) { - async.parallel([ - waitFor('leaves'), - enter(leavesRealtime) - ], cb) - }, - function(cb) { - mainChannel.presence.get(function(err, members) { - test.equal(members.length, 3, 'Check all three expected members here'); - cb(err); - }); - }, - function(cb) { - helper.becomeSuspended(mainRealtime, cb); - }, - function(cb) { - mainChannel.presence.get(function(err) { - /* Check RTP11d: get() returns an error by default */ - test.ok(err, 'Check error returned by get() while suspended'); - test.equal(err && err.code, 91005, 'Check error code for get() while suspended'); - cb(); - }); - }, - function(cb) { - mainChannel.presence.get({ waitForSync: false }, function(err, members) { - /* Check RTP11d: get() works while suspended if waitForSync: false */ - test.ok(!err, 'Check no error returned by get() while suspended if waitForSync: false'); - test.equal(members && members.length, 3, 'Check all three expected members here'); - cb(err); - }); - }, - function(cb) { - leavesRealtime.connection.whenState('closed', function() { cb(); }); - leavesRealtime.close(); - }, - function(cb) { - mainChannel.presence.subscribe(function(presmsg) { - test.equal(presmsg.clientId, 'leaves', 'Check the only presmsg we get is a leave from leaves'); - test.equal(presmsg.action, 'leave', 'Check the only presmsg we get is a leave from leaves'); - cb(); - }); - /* Don't need to reattach explicitly; should be done automatically on connected */ - mainRealtime.connect(); - }, - function(cb) { - /* Wait a bit to make sure we don't receive any other presence messages */ - setTimeout(cb, 1000); - }, - function(cb) { - mainChannel.presence.get(function(err, members) { - test.equal(members && members.length, 2, 'Check two expected members here'); - cb(err); - }); - } - ], function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } - closeAndFinish(test, [mainRealtime, continuousRealtime, leavesRealtime]); + async.series( + [ + enter(mainRealtime), + function (cb) { + async.parallel([waitFor('continuous'), enter(continuousRealtime)], cb); + }, + function (cb) { + async.parallel([waitFor('leaves'), enter(leavesRealtime)], cb); + }, + function (cb) { + mainChannel.presence.get(function (err, members) { + try { + expect(members.length).to.equal(3, 'Check all three expected members here'); + } catch (err) { + cb(err); + return; + } + cb(err); + }); + }, + function (cb) { + helper.becomeSuspended(mainRealtime, cb); + }, + function (cb) { + mainChannel.presence.get(function (err) { + /* Check RTP11d: get() returns an error by default */ + try { + expect(err, 'Check error returned by get() while suspended').to.be.ok; + expect(err && err.code).to.equal(91005, 'Check error code for get() while suspended'); + } catch (err) { + cb(err); + return; + } + cb(); + }); + }, + function (cb) { + mainChannel.presence.get({ waitForSync: false }, function (err, members) { + /* Check RTP11d: get() works while suspended if waitForSync: false */ + try { + expect(!err, 'Check no error returned by get() while suspended if waitForSync: false').to.be.ok; + expect(members && members.length).to.equal(3, 'Check all three expected members here'); + } catch (err) { + cb(err); + return; + } + cb(err); + }); + }, + function (cb) { + leavesRealtime.connection.whenState('closed', function () { + cb(); + }); + leavesRealtime.close(); + }, + function (cb) { + mainChannel.presence.subscribe(function (presmsg) { + try { + expect(presmsg.clientId).to.equal('leaves', 'Check the only presmsg we get is a leave from leaves'); + expect(presmsg.action).to.equal('leave', 'Check the only presmsg we get is a leave from leaves'); + } catch (err) { + cb(err); + return; + } + cb(); + }); + /* Don't need to reattach explicitly; should be done automatically on connected */ + mainRealtime.connect(); + }, + function (cb) { + /* Wait a bit to make sure we don't receive any other presence messages */ + setTimeout(cb, 1000); + }, + function (cb) { + mainChannel.presence.get(function (err, members) { + try { + expect(members && members.length).to.equal(2, 'Check two expected members here'); + } catch (err) { + cb(err); + return; + } + cb(err); + }); + } + ], + function (err) { + closeAndFinish(done, [mainRealtime, continuousRealtime, leavesRealtime], err); + } + ); }); - }; - /* - * Send >10 presence updates, check they were all broadcast. Point of this is - * to check for a bug in the original 0.8 spec re presence membersmap - * comparisons. - */ - exports.presence_many_updates = function(test) { - var client = helper.AblyRealtime({ clientId: testClientId }); - - test.expect(1); - var channel = client.channels.get('presence_many_updates'), - presence = channel.presence, - numUpdates = 0; - - channel.attach(function(err) { - if(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, client); - } - presence.subscribe(function(presMsg) { - numUpdates++; - }); + /* + * Send >10 presence updates, check they were all broadcast. Point of this is + * to check for a bug in the original 0.8 spec re presence membersmap + * comparisons. + */ + it('presence_many_updates', function (done) { + var client = helper.AblyRealtime({ clientId: testClientId }); - async.timesSeries(15, function(i, cb) { - presence.update(i.toString(), cb); - }, function(err) { - if(err) { test.ok(false, displayError(err)); } - // Wait to make sure everything has been received - setTimeout(function() { - test.equal(numUpdates, 15, 'Check got all the results'); - client.close(); - test.done(); - }, 1000); - }); - }) - }; + var channel = client.channels.get('presence_many_updates'), + presence = channel.presence, + numUpdates = 0; - helper.withMocha('realtime/presence', exports); -}); \ No newline at end of file + channel.attach(function (err) { + if (err) { + closeAndFinish(done, client, err); + } + presence.subscribe(function (presMsg) { + numUpdates++; + }); + + async.timesSeries( + 15, + function (i, cb) { + presence.update(i.toString(), cb); + }, + function (err) { + if (err) { + done(err); + return; + } + // Wait to make sure everything has been received + setTimeout(function () { + try { + expect(numUpdates).to.equal(15, 'Check got all the results'); + } catch (err) { + closeAndFinish(done, client, err); + return; + } + closeAndFinish(done, client); + }, 1000); + } + ); + }); + }); + }); +}); diff --git a/spec/realtime/reauth.test.js b/spec/realtime/reauth.test.js index 9bf64f8991..493821eb06 100644 --- a/spec/realtime/reauth.test.js +++ b/spec/realtime/reauth.test.js @@ -1,236 +1,259 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - clientId = 'testClientId', - rest, - mixin = helper.Utils.mixin, - displayError = helper.displayError; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { +'use strict'; + +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var expect = chai.expect; + var clientId = 'testClientId'; + var rest; + var mixin = helper.Utils.mixin; + var displayError = helper.displayError; + + describe('realtime/reauth', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } rest = helper.AblyRest(); - test.ok(true, 'Set up app'); - } - test.done(); + done(); + }); }); - }; - /* Waterfall helpers */ + /* Waterfall helpers */ - function getToken(tokenParams) { - return function(state, callback) { - rest.auth.requestToken(tokenParams, null, function(err, token) { - callback(err, mixin(state, {token: token})); - }); - }; - } - - function close() { - return function(state, callback) { - state.connectionMonitor && state.realtime.connection.off(state.connectionMonitor); - state.realtime.close(); - state.realtime.connection.once('closed', function() { - callback(null, state); - }); - }; - } - - function connectWithToken() { - return function(state, callback) { - var realtime = helper.AblyRealtime(mixin({token: state.token}, state.realtimeOpts)); - realtime.connection.once('connected', function() { - callback(null, mixin(state, {realtime: realtime})); - }); - }; - } - - /* For when connection should stay connected right through till it's closed */ - function monitorConnectionContinuity() { - return function(state, callback) { - var listener = function () { - if(this.event !== 'update') { - callback('Connection monitor: connection state changed to ' + this.event); - } + function getToken(tokenParams) { + return function (state, callback) { + rest.auth.requestToken(tokenParams, null, function (err, token) { + callback(err, mixin(state, { token: token })); + }); }; - state.realtime.connection.on(listener); - callback(null, mixin(state, {connectionMonitor: listener})); - }; - } - - function reauthWithToken() { - return function(state, callback) { - /* Callback once both authorize callback has callback and got 'update' - * event. (Latter will only happen one event loop cycle after the former; - * using async.parallel lets us test the update event without race - * conditions) */ - async.parallel([ - function(cb) { - state.realtime.auth.authorize(null, {token: state.token}, cb); - }, - function(cb) { - state.realtime.connection.on('update', function(stateChange) { - cb(stateChange.reason); - }); - } - ], function(err) { - callback(err, state); - }) - }; - } - - function attach(channelName) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName); - channel.attach(function(err) { - callback(err, state); - }); - }; - } - - function checkChannelState(channelName, expected) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName); - var err = channel.state === expected ? null : 'checkChannelState: channel state was ' + channel.state + ', expected ' + expected; - callback(err, state); - }; - } - function checkAttached(channelName) { return checkChannelState(channelName, 'attached'); } - - function waitChannelState(channelName, expected) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName); - if(channel.state === expected) { - callback(null, state); - return; - } - var timeout = setTimeout(function() { - channel.off(); - callback('waitChannelState: expected state not reached within 5s. Expected ' + expected + ', currently ' + channel.state); - }, 5000); - channel.once(function() { - clearTimeout(timeout); - waitChannelState(channelName, expected)(state, callback); - }); - }; - } - - function checkChannelErrorCode(channelName, expected) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName), - code = channel.errorReason && channel.errorReason.code; - var err = code === expected ? null : 'checkChannelErrorCode: channel error code was ' + code + ', expected ' + expected; - callback(err, state); - }; - } - - function checkCantAttach(channelName) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName); - channel.attach(function(err) { - if(err && err.code === 40160) { + } + + function close() { + return function (state, callback) { + state.connectionMonitor && state.realtime.connection.off(state.connectionMonitor); + state.realtime.close(); + state.realtime.connection.once('closed', function () { callback(null, state); - } else { - callback(err || 'checkCantAttach: unexpectedly allowed to attach'); - } - }); - }; - } + }); + }; + } + + function connectWithToken() { + return function (state, callback) { + var realtime = helper.AblyRealtime(mixin({ token: state.token }, state.realtimeOpts)); + realtime.connection.once('connected', function () { + callback(null, mixin(state, { realtime: realtime })); + }); + }; + } + + /* For when connection should stay connected right through till it's closed */ + function monitorConnectionContinuity() { + return function (state, callback) { + var listener = function () { + if (this.event !== 'update') { + callback('Connection monitor: connection state changed to ' + this.event); + } + }; + state.realtime.connection.on(listener); + callback(null, mixin(state, { connectionMonitor: listener })); + }; + } + + function reauthWithToken() { + return function (state, callback) { + /* Callback once both authorize callback has callback and got 'update' + * event. (Latter will only happen one event loop cycle after the former; + * using async.parallel lets us test the update event without race + * conditions) */ + async.parallel( + [ + function (cb) { + state.realtime.auth.authorize(null, { token: state.token }, cb); + }, + function (cb) { + state.realtime.connection.on('update', function (stateChange) { + cb(stateChange.reason); + }); + } + ], + function (err) { + callback(err, state); + } + ); + }; + } - function checkCanPublish(channelName) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName); - channel.publish(null, null, function(err) { + function attach(channelName) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName); + channel.attach(function (err) { + callback(err, state); + }); + }; + } + + function checkChannelState(channelName, expected) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName); + var err = + channel.state === expected + ? null + : 'checkChannelState: channel state was ' + channel.state + ', expected ' + expected; callback(err, state); - }); - }; - } - - function checkCantPublish(channelName) { - return function(state, callback) { - var channel = state.realtime.channels.get(channelName); - channel.publish(null, null, function(err) { - if(err && err.code === 40160) { + }; + } + function checkAttached(channelName) { + return checkChannelState(channelName, 'attached'); + } + + function waitChannelState(channelName, expected) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName); + if (channel.state === expected) { callback(null, state); - } else { - callback(err || 'checkCantPublish: unexpectedly allowed to publish'); + return; } + var timeout = setTimeout(function () { + channel.off(); + callback( + 'waitChannelState: expected state not reached within 5s. Expected ' + + expected + + ', currently ' + + channel.state + ); + }, 5000); + channel.once(function () { + clearTimeout(timeout); + waitChannelState(channelName, expected)(state, callback); + }); + }; + } + + function checkChannelErrorCode(channelName, expected) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName), + code = channel.errorReason && channel.errorReason.code; + var err = + code === expected ? null : 'checkChannelErrorCode: channel error code was ' + code + ', expected ' + expected; + callback(err, state); + }; + } + + function checkCantAttach(channelName) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName); + channel.attach(function (err) { + if (err && err.code === 40160) { + callback(null, state); + } else { + callback(err || 'checkCantAttach: unexpectedly allowed to attach'); + } + }); + }; + } + + function checkCanPublish(channelName) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName); + channel.publish(null, null, function (err) { + callback(err, state); + }); + }; + } + + function checkCantPublish(channelName) { + return function (state, callback) { + var channel = state.realtime.channels.get(channelName); + channel.publish(null, null, function (err) { + if (err && err.code === 40160) { + callback(null, state); + } else { + callback(err || 'checkCantPublish: unexpectedly allowed to publish'); + } + }); + }; + } + + function testCase(name, steps) { + helper.testOnAllTransports(name, function (realtimeOpts) { + return function (done) { + var _steps = steps.slice(); + _steps.unshift(function (cb) { + cb(null, { realtimeOpts: realtimeOpts }); + }); + async.waterfall(_steps, function (err) { + try { + expect(!err, err && name + ': ' + displayError(err)).to.be.ok; + } catch (err) { + done(err); + return; + } + done(); + }); + }; }); - }; - } - - function testCase(name, steps) { - helper.testOnAllTransports(exports, name, function(realtimeOpts) { return function(test) { - test.expect(1); - var _steps = steps.slice(); - _steps.unshift(function(cb) { cb(null, {realtimeOpts: realtimeOpts}); }); - async.waterfall(_steps, function(err) { - test.ok(!err, err && (name + ': ' + displayError(err))); - test.done(); - }); - };}); - } - - /**************** - ***** Tests ***** - ****************/ - - /* RTC8a1 */ - testCase('reauthCapabilityUpgradeNewChannel', [ - getToken({clientId: clientId, capability: {'wrongchannel': ['*']}}), - connectWithToken(), - monitorConnectionContinuity(), - checkCantAttach('rightchannel'), - getToken({clientId: clientId, capability: {'wrongchannel': ['*'], 'rightchannel': ['*']}}), - reauthWithToken(), - attach('rightchannel'), - close() - ]); - - /* RTC8a1 */ - testCase('reauthCapabilityDowngradeFullChannel', [ - getToken({clientId: clientId, capability: {'channel': ['*'], 'another': ['*']}}), - connectWithToken(), - monitorConnectionContinuity(), - attach('channel'), - getToken({clientId: clientId, capability: {'another': ['*']}}), - reauthWithToken(), - waitChannelState('channel', 'failed'), - checkChannelErrorCode('channel', 40160), - checkCantAttach('channel'), - close() - ]); - - testCase('reauthCapabilityUpgradeAddPublish', [ - getToken({clientId: clientId, capability: {'channel': ['subscribe']}}), - connectWithToken(), - monitorConnectionContinuity(), - attach('channel'), - checkCantPublish('channel'), - getToken({clientId: clientId, capability: {'channel': ['subscribe','publish']}}), - reauthWithToken(), - checkAttached('channel'), - checkCanPublish('channel'), - close() - ]); - - testCase('reauthCapabilityDowngradePublish', [ - getToken({clientId: clientId, capability: {'channel': ['subscribe','publish']}}), - connectWithToken(), - monitorConnectionContinuity(), - attach('channel'), - checkCanPublish('channel'), - getToken({clientId: clientId, capability: {'channel': ['subscribe']}}), - reauthWithToken(), - attach('channel'), - checkAttached('channel'), - checkCantPublish('channel'), - close() - ]); - - helper.withMocha('realtime/reauth', exports); -}); \ No newline at end of file + } + + /**************** + ***** Tests ***** + ****************/ + + /* RTC8a1 */ + testCase('reauthCapabilityUpgradeNewChannel', [ + getToken({ clientId: clientId, capability: { wrongchannel: ['*'] } }), + connectWithToken(), + monitorConnectionContinuity(), + checkCantAttach('rightchannel'), + getToken({ clientId: clientId, capability: { wrongchannel: ['*'], rightchannel: ['*'] } }), + reauthWithToken(), + attach('rightchannel'), + close() + ]); + + /* RTC8a1 */ + testCase('reauthCapabilityDowngradeFullChannel', [ + getToken({ clientId: clientId, capability: { channel: ['*'], another: ['*'] } }), + connectWithToken(), + monitorConnectionContinuity(), + attach('channel'), + getToken({ clientId: clientId, capability: { another: ['*'] } }), + reauthWithToken(), + waitChannelState('channel', 'failed'), + checkChannelErrorCode('channel', 40160), + checkCantAttach('channel'), + close() + ]); + + testCase('reauthCapabilityUpgradeAddPublish', [ + getToken({ clientId: clientId, capability: { channel: ['subscribe'] } }), + connectWithToken(), + monitorConnectionContinuity(), + attach('channel'), + checkCantPublish('channel'), + getToken({ clientId: clientId, capability: { channel: ['subscribe', 'publish'] } }), + reauthWithToken(), + checkAttached('channel'), + checkCanPublish('channel'), + close() + ]); + + testCase('reauthCapabilityDowngradePublish', [ + getToken({ clientId: clientId, capability: { channel: ['subscribe', 'publish'] } }), + connectWithToken(), + monitorConnectionContinuity(), + attach('channel'), + checkCanPublish('channel'), + getToken({ clientId: clientId, capability: { channel: ['subscribe'] } }), + reauthWithToken(), + attach('channel'), + checkAttached('channel'), + checkCantPublish('channel'), + close() + ]); + }); +}); + diff --git a/spec/realtime/resume.test.js b/spec/realtime/resume.test.js index 82dffe53e0..e60b9b8f42 100644 --- a/spec/realtime/resume.test.js +++ b/spec/realtime/resume.test.js @@ -1,547 +1,634 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - _exports = {}, - closeAndFinish = helper.closeAndFinish, - displayError = helper.displayError, - monitorConnection = helper.monitorConnection, - simulateDroppedConnection = helper.simulateDroppedConnection, - testOnAllTransports = helper.testOnAllTransports, - bestTransport = helper.bestTransport; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); - }; - - function mixin(target, src) { - for(var prop in src) - target[prop] = src[prop]; - return target; - } - - function sendAndAwait(message, sendingChannel, receivingChannel, callback) { - var event = String(Math.random()); - receivingChannel.subscribe(event, function(msg) { - console.log('received ' + msg.data + ' at ' + (new Date()).toString()); - receivingChannel.unsubscribe(event); - callback(); - }); - console.log('sending ' + message + ' at ' + (new Date()).toString()); - sendingChannel.publish(event, message, function(err) { - if(err) callback(err); +'use strict'; + +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var simulateDroppedConnection = helper.simulateDroppedConnection; + var testOnAllTransports = helper.testOnAllTransports; + var bestTransport = helper.bestTransport; + + describe('realtime/resume', function () { + this.timeout(120 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); }); - } - /** - * Empty resume case - * Send 5 messages; disconnect; reconnect; send 5 messages - */ - function resume_inactive(test, channelName, txOpts, rxOpts) { - var count = 5; + function mixin(target, src) { + for (var prop in src) target[prop] = src[prop]; + return target; + } - var txRest = helper.AblyRest(mixin(txOpts)); - var rxRealtime = helper.AblyRealtime(mixin(rxOpts)); - test.expect(3); + function sendAndAwait(message, sendingChannel, receivingChannel, callback) { + var event = String(Math.random()); + receivingChannel.subscribe(event, function (msg) { + receivingChannel.unsubscribe(event); + callback(); + }); + sendingChannel.publish(event, message, function (err) { + if (err) callback(err); + }); + } - var rxChannel = rxRealtime.channels.get(channelName); - var txChannel = txRest.channels.get(channelName); - var rxCount = 0; + /** + * Empty resume case + * Send 5 messages; disconnect; reconnect; send 5 messages + */ + function resume_inactive(done, channelName, txOpts, rxOpts) { + var count = 5; - function phase0(callback) { - rxChannel.attach(callback); - } + var txRest = helper.AblyRest(mixin(txOpts)); + var rxRealtime = helper.AblyRealtime(mixin(rxOpts)); - function phase1(callback) { - function ph1TxOnce() { - sendAndAwait('phase 1, message ' + rxCount, txChannel, rxChannel, function(err) { - if(err) callback(err); - if(++rxCount == count) { - console.log("phase 1 sent all messages, time: ", (new Date()).toString()) - test.ok(true, "phase 1: sent and received all messages") - callback(null); - return; - } - setTimeout(ph1TxOnce, 800); - }) + var rxChannel = rxRealtime.channels.get(channelName); + var txChannel = txRest.channels.get(channelName); + var rxCount = 0; + + function phase0(callback) { + rxChannel.attach(callback); } - ph1TxOnce(); - } - function phase2(callback) { - console.log("starting phase 2, time: ", (new Date()).toString()) - simulateDroppedConnection(rxRealtime); - /* continue in 5 seconds */ - setTimeout(callback, 5000); - } + function phase1(callback) { + function ph1TxOnce() { + sendAndAwait('phase 1, message ' + rxCount, txChannel, rxChannel, function (err) { + if (err) callback(err); + if (++rxCount == count) { + callback(null); + return; + } + setTimeout(ph1TxOnce, 800); + }); + } + ph1TxOnce(); + } - function phase3(callback) { - console.log("starting phase 3, time: ", (new Date()).toString()) - /* re-open the connection, verify resume mode */ - rxRealtime.connection.connect(); - var connectionManager = rxRealtime.connection.connectionManager; - connectionManager.once('transport.active', function(transport) { - test.equal(transport.params.mode, 'resume', 'Verify reconnect is resume mode'); - callback(null); - }); - } + function phase2(callback) { + simulateDroppedConnection(rxRealtime); + /* continue in 5 seconds */ + setTimeout(callback, 5000); + } - function phase4(callback) { - console.log("starting phase 4, time: ", (new Date()).toString()) - rxCount = 0; - function ph4TxOnce() { - sendAndAwait('phase 4, message ' + rxCount, txChannel, rxChannel, function(err) { - if(err) callback(err); - if(++rxCount == count) { - console.log("phase 4 sent all messages, time: ", (new Date()).toString()) - test.ok(true, "phase 4: sent and received all messages") - callback(null); + function phase3(callback) { + /* re-open the connection, verify resume mode */ + rxRealtime.connection.connect(); + var connectionManager = rxRealtime.connection.connectionManager; + connectionManager.once('transport.active', function (transport) { + try { + expect(transport.params.mode).to.equal('resume', 'Verify reconnect is resume mode'); + } catch (err) { + callback(err); return; } - setTimeout(ph4TxOnce, 800); - }) + callback(null); + }); } - ph4TxOnce(); - } - phase0(function(err) { - if(err) { - test.ok(false, 'Phase 1 failed with err: ' + displayError(err)); - test.done(); - return; + function phase4(callback) { + rxCount = 0; + function ph4TxOnce() { + sendAndAwait('phase 4, message ' + rxCount, txChannel, rxChannel, function (err) { + if (err) callback(err); + if (++rxCount == count) { + callback(null); + return; + } + setTimeout(ph4TxOnce, 800); + }); + } + ph4TxOnce(); } - phase1(function(err) { - if(err) { - test.ok(false, 'Phase 1 failed with err: ' + displayError(err)); - test.done(); + + phase0(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - phase2(function(err) { - if(err) { - test.ok(false, 'Phase 2 failed with err: ' + displayError(err)); - test.done(); + phase1(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - phase3(function(err) { - if(err) { - test.ok(false, 'Phase 3 failed with err: ' + displayError(err)); - test.done(); + phase2(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - phase4(function(err) { - if(err) { - test.ok(false, 'Phase 4 failed with err: ' + displayError(err)); + phase3(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - closeAndFinish(test, rxRealtime); + phase4(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); + return; + } + closeAndFinish(done, rxRealtime); + }); }); }); }); }); - }); - } - - testOnAllTransports(exports, 'resume_inactive', function(realtimeOpts) { return function(test) { - resume_inactive(test, 'resume_inactive' + String(Math.random()), {}, realtimeOpts); - }}, /* excludeUpgrade: */ true); + } - /** - * Simple resume case - * Send 5 messages; disconnect; send 5 messages; reconnect - */ - function resume_active(test, channelName, txOpts, rxOpts) { - var count = 5; + testOnAllTransports( + 'resume_inactive', + function (realtimeOpts) { + return function (done) { + resume_inactive(done, 'resume_inactive' + String(Math.random()), {}, realtimeOpts); + }; + }, + /* excludeUpgrade: */ true + ); - var txRest = helper.AblyRest(mixin(txOpts)); - var rxRealtime = helper.AblyRealtime(mixin(rxOpts)); - test.expect(3); + /** + * Simple resume case + * Send 5 messages; disconnect; send 5 messages; reconnect + */ + function resume_active(done, channelName, txOpts, rxOpts) { + var count = 5; - var rxChannel = rxRealtime.channels.get(channelName); - var txChannel = txRest.channels.get(channelName); - var rxCount = 0; + var txRest = helper.AblyRest(mixin(txOpts)); + var rxRealtime = helper.AblyRealtime(mixin(rxOpts)); - function phase0(callback) { - rxChannel.attach(callback); - } + var rxChannel = rxRealtime.channels.get(channelName); + var txChannel = txRest.channels.get(channelName); + var rxCount = 0; - function phase1(callback) { - function ph1TxOnce() { - sendAndAwait('phase 1, message ' + rxCount, txChannel, rxChannel, function(err) { - if(err) callback(err); - if(++rxCount == count) { - console.log("phase 1 sent all messages, time: ", (new Date()).toString()) - test.ok(true, "phase 1: sent and received all messages") - callback(null); - return; - } - setTimeout(ph1TxOnce, 800); - }) + function phase0(callback) { + rxChannel.attach(callback); } - ph1TxOnce(); - } - function phase2(callback) { - /* disconnect the transport and send 5 more messages - * NOTE: this uses knowledge of the internal operation - * of the client library to simulate a dropped connection - * without explicitly closing the connection */ - simulateDroppedConnection(rxRealtime); - var txCount = 0; - - function ph2TxOnce() { - console.log('sending (phase 2): ' + txCount); - txChannel.publish('sentWhileDisconnected', 'phase 2, message ' + txCount, function(err) { - if(err) callback(err); - }); - if(++txCount == count) { - /* sent all messages */ - setTimeout(function() { callback(null); }, 1000); - return; + function phase1(callback) { + function ph1TxOnce() { + sendAndAwait('phase 1, message ' + rxCount, txChannel, rxChannel, function (err) { + if (err) callback(err); + if (++rxCount == count) { + callback(null); + return; + } + setTimeout(ph1TxOnce, 800); + }); } - setTimeout(ph2TxOnce, 1000); + ph1TxOnce(); } - setTimeout(ph2TxOnce, 800); - } + function phase2(callback) { + /* disconnect the transport and send 5 more messages + * NOTE: this uses knowledge of the internal operation + * of the client library to simulate a dropped connection + * without explicitly closing the connection */ + simulateDroppedConnection(rxRealtime); + var txCount = 0; + + function ph2TxOnce() { + txChannel.publish('sentWhileDisconnected', 'phase 2, message ' + txCount, function (err) { + if (err) callback(err); + }); + if (++txCount == count) { + /* sent all messages */ + setTimeout(function () { + callback(null); + }, 1000); + return; + } + setTimeout(ph2TxOnce, 1000); + } - function phase3(callback) { - /* subscribe, re-open the connection, verify resume mode */ - rxChannel.subscribe('sentWhileDisconnected', function(msg) { - console.log('received ' + msg.data + ' at ' + (new Date()).toString()); - ++rxCount; - }); - rxCount = 0; - rxRealtime.connection.connect(); - var connectionManager = rxRealtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - test.equal(transport.params.mode, 'resume', 'Verify reconnect is resume mode'); - setTimeout(function() { - test.equal(rxCount, count, 'Verify Phase 3 messages all received'); - callback(null); - }, 2000); - }); - } + setTimeout(ph2TxOnce, 800); + } - phase0(function(err) { - if(err) { - test.ok(false, 'Phase 1 failed with err: ' + displayError(err)); - closeAndFinish(test, rxRealtime); - return; + function phase3(callback) { + /* subscribe, re-open the connection, verify resume mode */ + rxChannel.subscribe('sentWhileDisconnected', function (msg) { + ++rxCount; + }); + rxCount = 0; + rxRealtime.connection.connect(); + var connectionManager = rxRealtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + try { + expect(transport.params.mode).to.equal('resume', 'Verify reconnect is resume mode'); + } catch (err) { + callback(err); + return; + } + setTimeout(function () { + try { + expect(rxCount).to.equal(count, 'Verify Phase 3 messages all received'); + } catch (err) { + callback(err); + return; + } + callback(null); + }, 2000); + }); } - phase1(function(err) { - if(err) { - test.ok(false, 'Phase 1 failed with err: ' + displayError(err)); - closeAndFinish(test, rxRealtime); + + phase0(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - phase2(function(err) { - if(err) { - test.ok(false, 'Phase 2 failed with err: ' + displayError(err)); - closeAndFinish(test, rxRealtime); + phase1(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - phase3(function(err) { - if(err) { - test.ok(false, 'Phase 3 failed with err: ' + displayError(err)); - closeAndFinish(test, rxRealtime); + phase2(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); return; } - closeAndFinish(test, rxRealtime); + phase3(function (err) { + if (err) { + closeAndFinish(done, rxRealtime, err); + return; + } + closeAndFinish(done, rxRealtime); + }); }); }); }); - }); - } - - testOnAllTransports(exports, 'resume_active', function(realtimeOpts) { return function(test) { - resume_active(test, 'resume_active' + String(Math.random()), {}, realtimeOpts); - }}, /* excludeUpgrade: */ true); - - - /* RTN15c3 - * Resume with loss of continuity - */ - testOnAllTransports(exports, 'resume_lost_continuity', function(realtimeOpts) { return function(test) { - var realtime = helper.AblyRealtime(realtimeOpts), - connection = realtime.connection, - attachedChannelName = 'resume_lost_continuity_attached', - suspendedChannelName = 'resume_lost_continuity_suspended', - attachedChannel = realtime.channels.get(attachedChannelName), - suspendedChannel = realtime.channels.get(suspendedChannelName); - - test.expect(6); - async.series([ - function(cb) { - connection.once('connected', function() { cb(); }); - }, - function(cb) { - suspendedChannel.state = 'suspended'; - attachedChannel.attach(cb); - }, - function(cb) { - /* Sabotage the resume */ - connection.connectionManager.connectionKey = '_____!ablyjs_test_fake-key____', - connection.connectionManager.connectionId = 'ablyjs_tes'; - connection.connectionManager.connectionSerial = 17; - connection.connectionManager.msgSerial = 15; - connection.once('disconnected', function() { cb(); }); - connection.connectionManager.disconnectAllTransports(); - }, - function(cb) { - connection.once('connected', function(stateChange) { - test.equal(stateChange.reason && stateChange.reason.code, 80008, 'Unable to recover connection correctly set in the stateChange'); - test.equal(attachedChannel.state, 'attaching', 'Attached channel went into attaching'); - test.equal(suspendedChannel.state, 'attaching', 'Suspended channel went into attaching'); - test.equal(connection.connectionManager.msgSerial, 0, 'Check msgSerial is reset to 0'); - test.equal(connection.connectionManager.connectionSerial, -1, 'Check connectionSerial is reset by the new CONNECTED'); - test.ok(connection.connectionManager.connectionId !== 'ablyjs_tes', 'Check connectionId is set by the new CONNECTED'); - cb(); - }); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - closeAndFinish(test, realtime); - }); - }}, true /* Use a fixed transport as attaches are resent when the transport changes */); - - /* RTN15c5 - * Resume with token error - */ - testOnAllTransports(exports, 'resume_token_error', function(realtimeOpts) { return function(test) { - var realtime = helper.AblyRealtime(mixin(realtimeOpts, {useTokenAuth: true})), - badtoken, - connection = realtime.connection; - - test.expect(2); - async.series([ - function(cb) { - connection.once('connected', function() { cb(); }); - }, - function(cb) { - realtime.auth.requestToken({ttl: 1}, null, function(err, token) { - badtoken = token; - cb(err); - }) + } + + testOnAllTransports( + 'resume_active', + function (realtimeOpts) { + return function (done) { + resume_active(done, 'resume_active' + String(Math.random()), {}, realtimeOpts); + }; }, - function(cb) { - /* Sabotage the resume - use a valid but now-expired token */ - realtime.auth.tokenDetails.token = badtoken.token - connection.once(function(stateChange) { - test.ok(stateChange.current, 'disconnected', 'check connection disconnects first'); - cb(); - }); - connection.connectionManager.disconnectAllTransports(); + /* excludeUpgrade: */ true + ); + + /* RTN15c3 + * Resume with loss of continuity + */ + testOnAllTransports( + 'resume_lost_continuity', + function (realtimeOpts) { + return function (done) { + var realtime = helper.AblyRealtime(realtimeOpts), + connection = realtime.connection, + attachedChannelName = 'resume_lost_continuity_attached', + suspendedChannelName = 'resume_lost_continuity_suspended', + attachedChannel = realtime.channels.get(attachedChannelName), + suspendedChannel = realtime.channels.get(suspendedChannelName); + + async.series( + [ + function (cb) { + connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + suspendedChannel.state = 'suspended'; + attachedChannel.attach(cb); + }, + function (cb) { + /* Sabotage the resume */ + (connection.connectionManager.connectionKey = '_____!ablyjs_test_fake-key____'), + (connection.connectionManager.connectionId = 'ablyjs_tes'); + connection.connectionManager.connectionSerial = 17; + connection.connectionManager.msgSerial = 15; + connection.once('disconnected', function () { + cb(); + }); + connection.connectionManager.disconnectAllTransports(); + }, + function (cb) { + connection.once('connected', function (stateChange) { + try { + expect(stateChange.reason && stateChange.reason.code).to.equal( + 80008, + 'Unable to recover connection correctly set in the stateChange' + ); + expect(attachedChannel.state).to.equal('attaching', 'Attached channel went into attaching'); + expect(suspendedChannel.state).to.equal('attaching', 'Suspended channel went into attaching'); + expect(connection.connectionManager.msgSerial).to.equal(0, 'Check msgSerial is reset to 0'); + expect(connection.connectionManager.connectionSerial).to.equal( + -1, + 'Check connectionSerial is reset by the new CONNECTED' + ); + expect( + connection.connectionManager.connectionId !== 'ablyjs_tes', + 'Check connectionId is set by the new CONNECTED' + ).to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }; }, - function(cb) { - connection.once('connected', function(stateChange) { - test.ok(true, 'successfully reconnected after getting a new token'); - cb(); - }); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - closeAndFinish(test, realtime); - }); - }}, true); - - /* RTN15c4 - * Resume with fatal error - */ - testOnAllTransports(exports, 'resume_fatal_error', function(realtimeOpts) { return function(test) { - var realtime = helper.AblyRealtime(realtimeOpts), - connection = realtime.connection; - - test.expect(3); - async.series([ - function(cb) { - connection.once('connected', function() { cb(); }); + true /* Use a fixed transport as attaches are resent when the transport changes */ + ); + + /* RTN15c5 + * Resume with token error + */ + testOnAllTransports( + 'resume_token_error', + function (realtimeOpts) { + return function (done) { + var realtime = helper.AblyRealtime(mixin(realtimeOpts, { useTokenAuth: true })), + badtoken, + connection = realtime.connection; + + async.series( + [ + function (cb) { + connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + realtime.auth.requestToken({ ttl: 1 }, null, function (err, token) { + badtoken = token; + cb(err); + }); + }, + function (cb) { + /* Sabotage the resume - use a valid but now-expired token */ + realtime.auth.tokenDetails.token = badtoken.token; + connection.once(function (stateChange) { + try { + expect(stateChange.current, 'disconnected', 'check connection disconnects first').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + connection.connectionManager.disconnectAllTransports(); + }, + function (cb) { + connection.once('connected', function (stateChange) { + cb(); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }; }, - function(cb) { - var keyName = realtime.auth.key.split(':')[0]; - realtime.auth.key = keyName+ ':wrong'; - connection.once(function(stateChange) { - test.ok(stateChange.current, 'disconnected', 'check connection disconnects first'); - cb(); - }); - connection.connectionManager.disconnectAllTransports(); + true + ); + + /* RTN15c4 + * Resume with fatal error + */ + testOnAllTransports( + 'resume_fatal_error', + function (realtimeOpts) { + return function (done) { + var realtime = helper.AblyRealtime(realtimeOpts), + connection = realtime.connection; + + async.series( + [ + function (cb) { + connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + var keyName = realtime.auth.key.split(':')[0]; + realtime.auth.key = keyName + ':wrong'; + connection.once(function (stateChange) { + try { + expect(stateChange.current, 'disconnected', 'check connection disconnects first').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + connection.connectionManager.disconnectAllTransports(); + }, + function (cb) { + connection.once('failed', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(40101, 'check correct code propogated'); + } catch (err) { + cb(err); + return; + } + cb(); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }; }, - function(cb) { - connection.once('failed', function(stateChange) { - test.ok(true, 'check connection failed'); - test.equal(stateChange.reason.code, 40101, 'check correct code propogated'); - cb(); - }); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - closeAndFinish(test, realtime); + true + ); + + /* RTL2f + * Check channel resumed flag + * TODO: enable once realtime supports this + */ + it('channel_resumed_flag', function (done) { + var realtime = helper.AblyRealtime(), + realtimeTwo, + recoveryKey, + connection = realtime.connection, + channelName = 'channel_resumed_flag', + channel = realtime.channels.get(channelName); + + async.series( + [ + function (cb) { + connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + channel.attach(); + channel.once('attached', function (stateChange) { + try { + expect(stateChange.resumed).to.equal(false, 'Check channel not resumed when first attached'); + } catch (err) { + cb(err); + return; + } + recoveryKey = connection.recoveryKey; + cb(); + }); + }, + function (cb) { + helper.becomeSuspended(realtime, cb); + }, + function (cb) { + realtimeTwo = helper.AblyRealtime({ recover: recoveryKey }); + realtimeTwo.connection.once('connected', function (stateChange) { + if (stateChange.reason) { + cb(stateChange.reason); + return; + } + cb(); + }); + }, + function (cb) { + var channelTwo = realtimeTwo.channels.get(channelName); + channelTwo.attach(); + channelTwo.once('attached', function (stateChange) { + try { + expect(stateChange.resumed).to.equal(true, 'Check resumed flag is true'); + } catch (err) { + cb(err); + return; + } + cb(); + }); + } + ], + function (err) { + closeAndFinish(done, [realtime, realtimeTwo], err); + } + ); }); - }}, true); - - /* RTL2f - * Check channel resumed flag - * TODO: enable once realtime supports this - */ - exports.channel_resumed_flag = function(test) { - var realtime = helper.AblyRealtime(), - realtimeTwo, - recoveryKey, - connection = realtime.connection, - channelName = 'channel_resumed_flag', - channel = realtime.channels.get(channelName); - - test.expect(2); - async.series([ - function(cb) { - connection.once('connected', function() { cb(); }); - }, - function(cb) { - channel.attach(); - channel.once('attached', function(stateChange) { - test.equal(stateChange.resumed, false, 'Check channel not resumed when first attached'); - recoveryKey = connection.recoveryKey; - cb(); - }); - }, - function(cb) { - helper.becomeSuspended(realtime, cb); - }, - function(cb) { - realtimeTwo = helper.AblyRealtime({recover: recoveryKey}); - realtimeTwo.connection.once('connected', function(stateChange) { - if(stateChange.reason) { - test.ok(false, 'Error while recovering: ' + JSON.stringify(stateChange.reason)); + + /* + * Check the library doesn't try to resume once the connectionStateTtl has expired + */ + it('no_resume_once_suspended', function (done) { + var realtime = helper.AblyRealtime(), + connection = realtime.connection, + channelName = 'no_resume_once_suspended'; + + async.series( + [ + function (cb) { + connection.once('connected', function () { + cb(); + }); + }, + function (cb) { + helper.becomeSuspended(realtime, cb); + }, + function (cb) { + realtime.connection.connectionManager.tryATransport = function (transportParams) { + try { + expect(transportParams.mode).to.equal('clean', 'Check library didn’t try to resume'); + } catch (err) { + cb(err); + return; + } + cb(); + }; + connection.connect(); } - cb(); - }); - }, - function(cb) { - var channelTwo = realtimeTwo.channels.get(channelName); - channelTwo.attach(); - channelTwo.once('attached', function(stateChange) { - test.equal(stateChange.resumed, true, 'Check resumed flag is true'); - cb(); - }); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - closeAndFinish(test, [realtime, realtimeTwo]); + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); }); - }; - - /* - * Check the library doesn't try to resume once the connectionStateTtl has expired - */ - exports.no_resume_once_suspended = function(test) { - var realtime = helper.AblyRealtime(), - connection = realtime.connection, - channelName = 'no_resume_once_suspended'; - - test.expect(1); - async.series([ - function(cb) { - connection.once('connected', function() { cb(); }); - }, - function(cb) { - helper.becomeSuspended(realtime, cb); - }, - function(cb) { - realtime.connection.connectionManager.tryATransport = function(transportParams) { - test.equal(transportParams.mode, 'clean', 'Check library didn’t try to resume'); - cb(); + + /* + * Check the library doesn't try to resume if the last known activity on the + * connection was > connectionStateTtl ago + */ + it('no_resume_last_activity', function (done) { + /* Specify a best transport so that upgrade activity doesn't reset the last activity timer */ + var realtime = helper.AblyRealtime({ transports: [bestTransport] }), + connection = realtime.connection, + connectionManager = connection.connectionManager; + + connection.once('connected', function () { + connectionManager.lastActivity = helper.Utils.now() - 10000000; + /* noop-out onProtocolMessage so that a DISCONNECTED message doesn't + * reset the last activity timer */ + connectionManager.activeProtocol.getTransport().onProtocolMessage = function () {}; + connectionManager.tryATransport = function (transportParams) { + try { + expect(transportParams.mode).to.equal('clean', 'Check library didn’t try to resume'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); }; - connection.connect(); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - closeAndFinish(test, realtime); - }); - }; - - /* - * Check the library doesn't try to resume if the last known activity on the - * connection was > connectionStateTtl ago - */ - exports.no_resume_last_activity = function(test) { - /* Specify a best transport so that upgrade activity doesn't reset the last activity timer */ - var realtime = helper.AblyRealtime({transports: [bestTransport]}), - connection = realtime.connection, - connectionManager = connection.connectionManager - - test.expect(1); - connection.once('connected', function() { - connectionManager.lastActivity = helper.Utils.now() - 10000000 - /* noop-out onProtocolMessage so that a DISCONNECTED message doesn't - * reset the last activity timer */ - connectionManager.activeProtocol.getTransport().onProtocolMessage = function(){}; - connectionManager.tryATransport = function(transportParams) { - test.equal(transportParams.mode, 'clean', 'Check library didn’t try to resume'); - closeAndFinish(test, realtime); - }; - connectionManager.disconnectAllTransports(); + connectionManager.disconnectAllTransports(); + }); }); - }; - - exports.resume_rewind_1 = function(test) { - - test.expect(2); - - var testName = 'resume_rewind_1'; - var testMessage = { foo: 'bar', count: 1, status: 'active' }; - try { - - var sender_realtime = helper.AblyRealtime(); - var sender_channel = sender_realtime.channels.get(testName); - - sender_channel.subscribe(function(message) { - var receiver_realtime = helper.AblyRealtime(); - var receiver_channel = receiver_realtime.channels.get(testName, { params: { rewind: 1 } }); - - receiver_channel.subscribe(function(message) { - test.ok(JSON.stringify(testMessage) === JSON.stringify(message.data), 'Check rewound message.data'); - - var resumed_receiver_realtime = helper.AblyRealtime(); - var connectionManager = resumed_receiver_realtime.connection.connectionManager; - - var sendOrig = connectionManager.send; - connectionManager.send = function(msg, queueEvent, callback) { - msg.setFlag('ATTACH_RESUME'); - sendOrig.call(connectionManager, msg, queueEvent, callback); - }; - - var resumed_receiver_channel = resumed_receiver_realtime.channels.get(testName, { params: { rewind: 1 } }); - - resumed_receiver_channel.subscribe(function(message) { - clearTimeout(success); - test.ok(false, 'Rewound message arrived on attach resume'); - closeAndFinish(test, [sender_realtime, receiver_realtime, resumed_receiver_realtime]); + + it('resume_rewind_1', function (done) { + var testName = 'resume_rewind_1'; + var testMessage = { foo: 'bar', count: 1, status: 'active' }; + try { + var sender_realtime = helper.AblyRealtime(); + var sender_channel = sender_realtime.channels.get(testName); + + sender_channel.subscribe(function (message) { + var receiver_realtime = helper.AblyRealtime(); + var receiver_channel = receiver_realtime.channels.get(testName, { params: { rewind: 1 } }); + + receiver_channel.subscribe(function (message) { + try { + expect( + JSON.stringify(testMessage) === JSON.stringify(message.data), + 'Check rewound message.data' + ).to.be.ok; + } catch (err) { + closeAndFinish(done, [sender_realtime, receiver_realtime], err); + return; + } + + var resumed_receiver_realtime = helper.AblyRealtime(); + var connectionManager = resumed_receiver_realtime.connection.connectionManager; + + var sendOrig = connectionManager.send; + connectionManager.send = function (msg, queueEvent, callback) { + msg.setFlag('ATTACH_RESUME'); + sendOrig.call(connectionManager, msg, queueEvent, callback); + }; + + var resumed_receiver_channel = resumed_receiver_realtime.channels.get(testName, { params: { rewind: 1 } }); + + resumed_receiver_channel.subscribe(function (message) { + clearTimeout(success); + closeAndFinish( + done, + [sender_realtime, receiver_realtime, resumed_receiver_realtime], + new Error('Rewound message arrived on attach resume') + ); + }); + + var success = setTimeout(function() { + closeAndFinish(done, [sender_realtime, receiver_realtime, resumed_receiver_realtime]); + }, 7000); }); + }); - var success = setTimeout(() => { - test.ok(true); - closeAndFinish(test, [sender_realtime, receiver_realtime, resumed_receiver_realtime]); - }, 7000); - + sender_realtime.connection.on('connected', function () { + sender_channel.publish('0', testMessage); }); - }); - - sender_realtime.connection.on('connected', function() { - sender_channel.publish('0', testMessage); - }); - - } catch(e) { - test.ok(false, testName + ' test failed with exception: ' + e.stack); - closeAndFinish(test, [sender_realtime, receiver_realtime, resumed_receiver_realtime]); - } - } + } catch (err) { + closeAndFinish(done, [sender_realtime, receiver_realtime, resumed_receiver_realtime], err); + } + }); + }); +}); - helper.withMocha('realtime/resume', exports, 120 * 1000); // allow 2 minutes for some of the longer phased tests -}); \ No newline at end of file diff --git a/spec/realtime/sync.test.js b/spec/realtime/sync.test.js index 475360c630..dbd71a0b54 100644 --- a/spec/realtime/sync.test.js +++ b/spec/realtime/sync.test.js @@ -1,529 +1,628 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - _exports = {}, - displayError = helper.displayError, - utils = helper.Utils, - closeAndFinish = helper.closeAndFinish, - createPM = Ably.Realtime.ProtocolMessage.fromDeserialized, - monitorConnection = helper.monitorConnection; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - test.ok(!err, 'app set up ' + (err && displayError(err))); - test.done(); +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + var displayError = helper.displayError; + var utils = helper.Utils; + var closeAndFinish = helper.closeAndFinish; + var createPM = Ably.Realtime.ProtocolMessage.fromDeserialized; + var monitorConnection = helper.monitorConnection; + + describe('realtime/sync', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); }); - }; - function extractClientIds(presenceSet) { - return utils.arrMap(presenceSet, function(presmsg) { - return presmsg.clientId; - }).sort(); - } + function extractClientIds(presenceSet) { + return utils + .arrMap(presenceSet, function (presmsg) { + return presmsg.clientId; + }) + .sort(); + } - function extractMember(presenceSet, clientId) { - return helper.arrFind(presenceSet, function(member) { - return member.clientId === clientId; - }); - } - - /* - * Sync with an existing presence set - should discard any member who wasn't - * included in the sync. - * Note: doesn't use a real connection as everything's being done with - * simulated protocol messages. Start with a fake-attached channel with no - * sync in progress, then do one sync, then a second with a slightly - * different presence set - */ - exports.sync_existing_set = function(test) { - test.expect(6); - var realtime = helper.AblyRealtime({autoConnect: false}), - channelName = 'syncexistingset', - channel = realtime.channels.get(channelName); - - channel.onMessage(createPM({ - action: 11, - channel: channelName, - flags: 1 - })); - - async.series([ - function(cb) { - channel.onMessage({ - action: 16, - channel: channelName, - presence: [ - { - action: 'present', - clientId: 'one', - connectionId: 'one_connid', - id: 'one_connid:0:0', - timestamp: 1e12 - }, - { - action: 'present', - clientId: 'two', - connectionId: 'two_connid', - id: 'two_connid:0:0', - timestamp: 1e12 - } - ]}); - cb(); - }, - function(cb) { - channel.presence.get(function(err, results) { - test.equal(results.length, 2, 'Check correct number of results'); - test.ok(channel.presence.syncComplete, 'Check in sync'); - test.deepEqual(extractClientIds(results), ['one', 'two'], 'check correct members'); - cb(err); - }); - }, - function(cb) { - /* Trigger another sync. Two has gone without so much as a `leave` message! */ - channel.onMessage({ - action: 16, + function extractMember(presenceSet, clientId) { + return helper.arrFind(presenceSet, function (member) { + return member.clientId === clientId; + }); + } + + /* + * Sync with an existing presence set - should discard any member who wasn't + * included in the sync. + * Note: doesn't use a real connection as everything's being done with + * simulated protocol messages. Start with a fake-attached channel with no + * sync in progress, then do one sync, then a second with a slightly + * different presence set + */ + it('sync_existing_set', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + channelName = 'syncexistingset', + channel = realtime.channels.get(channelName); + + channel.onMessage( + createPM({ + action: 11, channel: channelName, - presence: [ - { - action: 'present', - clientId: 'one', - connectionId: 'one_connid', - id: 'one_connid:0:0', - timestamp: 1e12 - }, - { - action: 'present', - clientId: 'three', - connectionId: 'three_connid', - id: 'three_connid:0:0', - timestamp: 1e12 - } - ]}); - cb(); - }, - function(cb) { - channel.presence.get(function(err, results) { - test.equal(results.length, 2, 'Check correct number of results'); - test.ok(channel.presence.syncComplete, 'Check in sync'); - test.deepEqual(extractClientIds(results), ['one', 'three'], 'check two has gone and three is there'); - cb(err); - }); - } - ], function(err) { - if(err) test.ok(false, helper.displayError(err)); - test.done(); - }); - }; - - /* - * Sync with an existing presence set and a presence member added in the - * middle of the sync should should discard the former, but not the latter - * */ - exports.sync_member_arrives_in_middle = function(test) { - test.expect(3); - var realtime = helper.AblyRealtime({autoConnect: false}), - channelName = 'sync_member_arrives_in_middle', - channel = realtime.channels.get(channelName); - - channel.onMessage(createPM({ - action: 11, - channel: channelName, - flags: 1 - })); - - /* First sync */ - channel.onMessage({ - action: 16, - channel: channelName, - presence: [ - { - action: 'present', - clientId: 'one', - connectionId: 'one_connid', - id: 'one_connid:0:0', - timestamp: 1e12 - } - ]}); - - /* A second sync, this time in multiple parts, with a presence message in the middle */ - channel.onMessage({ - action: 16, - channel: channelName, - channelSerial: 'serial:cursor', - presence: [ - { - action: 'present', - clientId: 'two', - connectionId: 'two_connid', - id: 'two_connid:0:0', - timestamp: 1e12 - } - ]}); - - channel.onMessage({ - action: 14, - channel: channelName, - presence: [ - { - action: 'enter', - clientId: 'three', - connectionId: 'three_connid', - id: 'three_connid:0:0', - timestamp: 1e12 - } - ]}); - - channel.onMessage({ - action: 16, - channel: channelName, - channelSerial: 'serial:', - presence: [ - { - action: 'present', - clientId: 'four', - connectionId: 'four_connid', - id: 'four_connid:0:0', - timestamp: 1e12 + flags: 1 + }) + ); + + async.series( + [ + function (cb) { + channel.onMessage({ + action: 16, + channel: channelName, + presence: [ + { + action: 'present', + clientId: 'one', + connectionId: 'one_connid', + id: 'one_connid:0:0', + timestamp: 1e12 + }, + { + action: 'present', + clientId: 'two', + connectionId: 'two_connid', + id: 'two_connid:0:0', + timestamp: 1e12 + } + ] + }); + cb(); + }, + function (cb) { + channel.presence.get(function (err, results) { + try { + expect(results.length).to.equal(2, 'Check correct number of results'); + expect(channel.presence.syncComplete, 'Check in sync').to.be.ok; + expect(extractClientIds(results)).to.deep.equal(['one', 'two'], 'check correct members'); + } catch (err) { + cb(err); + return; + } + cb(err); + }); + }, + function (cb) { + /* Trigger another sync. Two has gone without so much as a `leave` message! */ + channel.onMessage({ + action: 16, + channel: channelName, + presence: [ + { + action: 'present', + clientId: 'one', + connectionId: 'one_connid', + id: 'one_connid:0:0', + timestamp: 1e12 + }, + { + action: 'present', + clientId: 'three', + connectionId: 'three_connid', + id: 'three_connid:0:0', + timestamp: 1e12 + } + ] + }); + cb(); + }, + function (cb) { + channel.presence.get(function (err, results) { + try { + expect(results.length).to.equal(2, 'Check correct number of results'); + expect(channel.presence.syncComplete, 'Check in sync').to.be.ok; + expect(extractClientIds(results)).to.deep.equal( + ['one', 'three'], + 'check two has gone and three is there' + ); + } catch (err) { + cb(err); + return; + } + cb(err); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); } - ]}); - - channel.presence.get(function(err, results) { - test.equal(results.length, 3, 'Check correct number of results'); - test.ok(channel.presence.syncComplete, 'Check in sync'); - test.deepEqual(extractClientIds(results), ['four', 'three', 'two'], 'check expected presence members'); - test.done(); + ); }); - }; - - /* - * Presence message that was in the sync arrives again as a normal message, after it's come in the sync - */ - exports.sync_member_arrives_normally_after_came_in_sync = function(test) { - test.expect(3); - var realtime = helper.AblyRealtime({autoConnect: false}), - channelName = 'sync_member_arrives_normally_after_came_in_sync', - channel = realtime.channels.get(channelName); - - channel.onMessage(createPM({ - action: 11, - channel: channelName, - flags: 1 - })); - - channel.onMessage({ - action: 16, - channel: channelName, - channelSerial: 'serial:cursor', - presence: [ - { - action: 'present', - clientId: 'one', - connectionId: 'one_connid', - id: 'one_connid:0:0', - timestamp: 1e12 - } - ]}); - - channel.onMessage({ - action: 14, - channel: channelName, - presence: [ - { - action: 'enter', - clientId: 'one', - connectionId: 'one_connid', - id: 'one_connid:0:0', - timestamp: 1e12 - } - ]}); - - channel.onMessage({ - action: 16, - channel: channelName, - channelSerial: 'serial:', - presence: [ - { - action: 'present', - clientId: 'two', - connectionId: 'two_connid', - id: 'two_connid:0:0', - timestamp: 1e12 - } - ]}); - channel.presence.get(function(err, results) { - test.equal(results.length, 2, 'Check correct number of results'); - test.ok(channel.presence.syncComplete, 'Check in sync'); - test.deepEqual(extractClientIds(results), ['one', 'two'], 'check expected presence members'); - test.done(); - }); - }; - - /* - * Presence message that will be in the sync arrives as a normal message, before it comes in the sync - */ - exports.sync_member_arrives_normally_before_comes_in_sync = function(test) { - test.expect(3); - var realtime = helper.AblyRealtime({autoConnect: false}), - channelName = 'sync_member_arrives_normally_before_comes_in_sync', - channel = realtime.channels.get(channelName); - - channel.onMessage(createPM({ - action: 11, - channel: channelName, - flags: 1 - })); - - channel.onMessage({ - action: 16, - channel: channelName, - channelSerial: 'serial:cursor', - presence: [ - { - action: 'present', - clientId: 'one', - connectionId: 'one_connid', - id: 'one_connid:0:0', - timestamp: 1e12 - } - ]}); - - channel.onMessage({ - action: 14, - channel: channelName, - presence: [ - { - action: 'enter', - clientId: 'two', - connectionId: 'two_connid', - id: 'two_connid:0:0', - timestamp: 1e12 + /* + * Sync with an existing presence set and a presence member added in the + * middle of the sync should should discard the former, but not the latter + * */ + it('sync_member_arrives_in_middle', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + channelName = 'sync_member_arrives_in_middle', + channel = realtime.channels.get(channelName); + + channel.onMessage( + createPM({ + action: 11, + channel: channelName, + flags: 1 + }) + ); + + /* First sync */ + channel.onMessage({ + action: 16, + channel: channelName, + presence: [ + { + action: 'present', + clientId: 'one', + connectionId: 'one_connid', + id: 'one_connid:0:0', + timestamp: 1e12 + } + ] + }); + + /* A second sync, this time in multiple parts, with a presence message in the middle */ + channel.onMessage({ + action: 16, + channel: channelName, + channelSerial: 'serial:cursor', + presence: [ + { + action: 'present', + clientId: 'two', + connectionId: 'two_connid', + id: 'two_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.onMessage({ + action: 14, + channel: channelName, + presence: [ + { + action: 'enter', + clientId: 'three', + connectionId: 'three_connid', + id: 'three_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.onMessage({ + action: 16, + channel: channelName, + channelSerial: 'serial:', + presence: [ + { + action: 'present', + clientId: 'four', + connectionId: 'four_connid', + id: 'four_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.presence.get(function (err, results) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - channel.onMessage({ - action: 16, - channel: channelName, - channelSerial: 'serial:', - presence: [ - { - action: 'present', - clientId: 'two', - connectionId: 'two_connid', - id: 'two_connid:0:0', - timestamp: 1e12 + try { + expect(results.length).to.equal(3, 'Check correct number of results'); + expect(channel.presence.syncComplete, 'Check in sync').to.be.ok; + expect(extractClientIds(results)).to.deep.equal(['four', 'three', 'two'], 'check expected presence members'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - channel.presence.get(function(err, results) { - test.equal(results.length, 2, 'Check correct number of results'); - test.ok(channel.presence.syncComplete, 'Check in sync'); - test.deepEqual(extractClientIds(results), ['one', 'two'], 'check expected presence members'); - test.done(); + closeAndFinish(done, realtime); + }); }); - }; - - /* - * Get several presence messages with various combinations of msgserial, - * index, and synthesized leaves, check that the end result is correct - */ - exports.presence_ordering = function(test) { - test.expect(5); - var realtime = helper.AblyRealtime({autoConnect: false}), - channelName = 'sync_ordering', - channel = realtime.channels.get(channelName); - - channel.onMessage(createPM({ - action: 11, - channel: channelName - })); - - /* One enters */ - channel.onMessage({ - action: 14, - channel: channelName, - id: 'one_connid:1', - connectionId: 'one_connid', - timestamp: 1e12, - presence: [ - { - action: 'enter', - clientId: 'one' - } - ]}); - - /* An earlier leave from one (should be ignored) */ - channel.onMessage({ - action: 14, - channel: channelName, - connectionId: 'one_connid', - id: 'one_connid:0', - timestamp: 1e12, - presence: [ - { - action: 'leave', - clientId: 'one' + + /* + * Presence message that was in the sync arrives again as a normal message, after it's come in the sync + */ + it('sync_member_arrives_normally_after_came_in_sync', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + channelName = 'sync_member_arrives_normally_after_came_in_sync', + channel = realtime.channels.get(channelName); + + channel.onMessage( + createPM({ + action: 11, + channel: channelName, + flags: 1 + }) + ); + + channel.onMessage({ + action: 16, + channel: channelName, + channelSerial: 'serial:cursor', + presence: [ + { + action: 'present', + clientId: 'one', + connectionId: 'one_connid', + id: 'one_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.onMessage({ + action: 14, + channel: channelName, + presence: [ + { + action: 'enter', + clientId: 'one', + connectionId: 'one_connid', + id: 'one_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.onMessage({ + action: 16, + channel: channelName, + channelSerial: 'serial:', + presence: [ + { + action: 'present', + clientId: 'two', + connectionId: 'two_connid', + id: 'two_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.presence.get(function (err, results) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - /* One adds some data in a newer msgSerial */ - channel.onMessage({ - action: 14, - channel: channelName, - connectionId: 'one_connid', - id: 'one_connid:2', - timestamp: 1e12, - presence: [ - { - action: 'update', - clientId: 'one', - data: 'onedata' + try { + expect(results.length).to.equal(2, 'Check correct number of results'); + expect(channel.presence.syncComplete, 'Check in sync').to.be.ok; + expect(extractClientIds(results)).to.deep.equal(['one', 'two'], 'check expected presence members'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - /* Two enters */ - channel.onMessage({ - action: 14, - channel: channelName, - connectionId: 'two_connid', - id: 'two_connid:0', - timestamp: 1e12, - presence: [ - { - action: 'enter', - clientId: 'two' + closeAndFinish(done, realtime); + }); + }); + + /* + * Presence message that will be in the sync arrives as a normal message, before it comes in the sync + */ + it('sync_member_arrives_normally_before_comes_in_sync', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + channelName = 'sync_member_arrives_normally_before_comes_in_sync', + channel = realtime.channels.get(channelName); + + channel.onMessage( + createPM({ + action: 11, + channel: channelName, + flags: 1 + }) + ); + + channel.onMessage({ + action: 16, + channel: channelName, + channelSerial: 'serial:cursor', + presence: [ + { + action: 'present', + clientId: 'one', + connectionId: 'one_connid', + id: 'one_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.onMessage({ + action: 14, + channel: channelName, + presence: [ + { + action: 'enter', + clientId: 'two', + connectionId: 'two_connid', + id: 'two_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.onMessage({ + action: 16, + channel: channelName, + channelSerial: 'serial:', + presence: [ + { + action: 'present', + clientId: 'two', + connectionId: 'two_connid', + id: 'two_connid:0:0', + timestamp: 1e12 + } + ] + }); + + channel.presence.get(function (err, results) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - /* Two updates twice in the same message */ - channel.onMessage({ - action: 14, - channel: channelName, - connectionId: 'two_connid', - id: 'two_connid:0', - timestamp: 1e12, - presence: [ - { - action: 'update', - clientId: 'two', - data: 'twowrongdata' - }, - { - action: 'update', - clientId: 'two', - data: 'twodata' + try { + expect(results.length).to.equal(2, 'Check correct number of results'); + expect(channel.presence.syncComplete, 'Check in sync').to.be.ok; + expect(extractClientIds(results)).to.deep.equal(['one', 'two'], 'check expected presence members'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - /* Three enters */ - channel.onMessage({ - action: 14, - channel: channelName, - connectionId: 'three_connid', - id: 'three_connid:99', - timestamp: 1e12, - presence: [ - { - action: 'enter', - clientId: 'three' + closeAndFinish(done, realtime); + }); + }); + + /* + * Get several presence messages with various combinations of msgserial, + * index, and synthesized leaves, check that the end result is correct + */ + it('presence_ordering', function (done) { + var realtime = helper.AblyRealtime({ autoConnect: false }), + channelName = 'sync_ordering', + channel = realtime.channels.get(channelName); + + channel.onMessage( + createPM({ + action: 11, + channel: channelName + }) + ); + + /* One enters */ + channel.onMessage({ + action: 14, + channel: channelName, + id: 'one_connid:1', + connectionId: 'one_connid', + timestamp: 1e12, + presence: [ + { + action: 'enter', + clientId: 'one' + } + ] + }); + + /* An earlier leave from one (should be ignored) */ + channel.onMessage({ + action: 14, + channel: channelName, + connectionId: 'one_connid', + id: 'one_connid:0', + timestamp: 1e12, + presence: [ + { + action: 'leave', + clientId: 'one' + } + ] + }); + + /* One adds some data in a newer msgSerial */ + channel.onMessage({ + action: 14, + channel: channelName, + connectionId: 'one_connid', + id: 'one_connid:2', + timestamp: 1e12, + presence: [ + { + action: 'update', + clientId: 'one', + data: 'onedata' + } + ] + }); + + /* Two enters */ + channel.onMessage({ + action: 14, + channel: channelName, + connectionId: 'two_connid', + id: 'two_connid:0', + timestamp: 1e12, + presence: [ + { + action: 'enter', + clientId: 'two' + } + ] + }); + + /* Two updates twice in the same message */ + channel.onMessage({ + action: 14, + channel: channelName, + connectionId: 'two_connid', + id: 'two_connid:0', + timestamp: 1e12, + presence: [ + { + action: 'update', + clientId: 'two', + data: 'twowrongdata' + }, + { + action: 'update', + clientId: 'two', + data: 'twodata' + } + ] + }); + + /* Three enters */ + channel.onMessage({ + action: 14, + channel: channelName, + connectionId: 'three_connid', + id: 'three_connid:99', + timestamp: 1e12, + presence: [ + { + action: 'enter', + clientId: 'three' + } + ] + }); + + /* Synthesized leave for three (with earlier msgSerial, incompatible id, + * and later timestamp) */ + channel.onMessage({ + action: 14, + channel: channelName, + connectionId: 'synthesized', + id: 'synthesized:0', + timestamp: 1e12 + 1, + presence: [ + { + action: 'leave', + clientId: 'three', + connectionId: 'three_connid' + } + ] + }); + + channel.presence.get(function (err, results) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - /* Synthesized leave for three (with earlier msgSerial, incompatible id, - * and later timestamp) */ - channel.onMessage({ - action: 14, - channel: channelName, - connectionId: 'synthesized', - id: 'synthesized:0', - timestamp: 1e12 + 1, - presence: [ - { - action: 'leave', - clientId: 'three', - connectionId: 'three_connid' + try { + expect(results.length).to.equal(2, 'Check correct number of results'); + expect(channel.presence.syncComplete, 'Check in sync').to.be.ok; + expect(extractClientIds(results)).to.deep.equal(['one', 'two'], 'check expected presence members'); + expect(extractMember(results, 'one').data).to.equal('onedata', 'check correct data on one'); + expect(extractMember(results, 'two').data).to.equal('twodata', 'check correct data on two'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; } - ]}); - - channel.presence.get(function(err, results) { - test.equal(results.length, 2, 'Check correct number of results'); - test.ok(channel.presence.syncComplete, 'Check in sync'); - test.deepEqual(extractClientIds(results), ['one', 'two'], 'check expected presence members'); - test.equal(extractMember(results, 'one').data, 'onedata', 'check correct data on one'); - test.equal(extractMember(results, 'two').data, 'twodata', 'check correct data on two'); - test.done(); + closeAndFinish(done, realtime); + }); }); - }; - - /* - * Do a 110-member sync, so split into two sync messages. Inject a normal - * presence enter between the syncs. Check everything was entered correctly - */ - exports.presence_sync_interruptus = function(test) { - if(helper.bestTransport === 'jsonp') { - /* JSONP can't cope with entering 110 people in one go. */ - console.log("Skipping presence_sync_interruptus test (jsonp)"); - test.done(); - return; - } - test.expect(1); - var channelName = "presence_sync_interruptus"; - var interrupterClientId = "dark_horse"; - var enterer = helper.AblyRealtime(); - var syncer = helper.AblyRealtime(); - var entererChannel = enterer.channels.get(channelName); - var syncerChannel = syncer.channels.get(channelName); - - function waitForBothConnect(cb) { - async.parallel([ - function(connectCb) { enterer.connection.on('connected', connectCb); }, - function(connectCb) { syncer.connection.on('connected', connectCb); } - ], function() { cb(); }); - } - async.series([ - waitForBothConnect, - function(cb) { entererChannel.attach(cb); }, - function(cb) { - async.times(110, function(i, presCb) { - entererChannel.presence.enterClient(i.toString(), null, presCb); - }, cb); - }, - function(cb) { - var originalOnMessage = syncerChannel.onMessage; - syncerChannel.onMessage = function(message) { - originalOnMessage.apply(this, arguments); - /* Inject an additional presence message after the first sync */ - if(message.action === 16) { - syncerChannel.onMessage = originalOnMessage; - syncerChannel.onMessage({ - "action": 14, - "id": "messageid:0", - "connectionId": "connid", - "timestamp": 2000000000000, - "presence": [{ - "clientId": interrupterClientId, - "action": 'enter' - }]}); + /* JSONP can't cope with entering 110 people in one go. */ + if (helper.bestTransport !== 'jsonp') { + /* + * Do a 110-member sync, so split into two sync messages. Inject a normal + * presence enter between the syncs. Check everything was entered correctly + */ + it('presence_sync_interruptus', function (done) { + var channelName = 'presence_sync_interruptus'; + var interrupterClientId = 'dark_horse'; + var enterer = helper.AblyRealtime(); + var syncer = helper.AblyRealtime(); + var entererChannel = enterer.channels.get(channelName); + var syncerChannel = syncer.channels.get(channelName); + + function waitForBothConnect(cb) { + async.parallel( + [ + function (connectCb) { + enterer.connection.on('connected', connectCb); + }, + function (connectCb) { + syncer.connection.on('connected', connectCb); + } + ], + function () { + cb(); + } + ); + } + + async.series( + [ + waitForBothConnect, + function (cb) { + entererChannel.attach(cb); + }, + function (cb) { + async.times( + 110, + function (i, presCb) { + entererChannel.presence.enterClient(i.toString(), null, presCb); + }, + cb + ); + }, + function (cb) { + var originalOnMessage = syncerChannel.onMessage; + syncerChannel.onMessage = function (message) { + originalOnMessage.apply(this, arguments); + /* Inject an additional presence message after the first sync */ + if (message.action === 16) { + syncerChannel.onMessage = originalOnMessage; + syncerChannel.onMessage({ + action: 14, + id: 'messageid:0', + connectionId: 'connid', + timestamp: 2000000000000, + presence: [ + { + clientId: interrupterClientId, + action: 'enter' + } + ] + }); + } + }; + syncerChannel.attach(cb); + }, + function (cb) { + syncerChannel.presence.get(function (err, presenceSet) { + try { + expect(presenceSet && presenceSet.length).to.equal(111, 'Check everyone’s in presence set'); + } catch (err) { + cb(err); + return; + } + cb(err); + }); + } + ], + function (err) { + closeAndFinish(done, [enterer, syncer], err); } - }; - syncerChannel.attach(cb); - }, - function(cb) { - syncerChannel.presence.get(function(err, presenceSet) { - test.equal(presenceSet && presenceSet.length, 111, 'Check everyone’s in presence set'); - cb(err); - }); - } - ], function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } - closeAndFinish(test, [enterer, syncer]); - }); - }; + ); + }); + } + }); +}); - helper.withMocha('realtime/sync', exports); -}); \ No newline at end of file diff --git a/spec/realtime/upgrade.test.js b/spec/realtime/upgrade.test.js index 178af303f0..61a779cb64 100644 --- a/spec/realtime/upgrade.test.js +++ b/spec/realtime/upgrade.test.js @@ -1,708 +1,757 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - _exports = {}, - rest, - publishIntervalHelper = function(currentMessageNum, channel, dataFn, onPublish){ - return function(currentMessageNum) { - channel.publish('event0', dataFn(), function() { - onPublish(); - }); - }; - }, - publishAtIntervals = function(numMessages, channel, dataFn, onPublish){ - for(var i = numMessages; i > 0; i--) { - setTimeout(publishIntervalHelper(i, channel, dataFn, onPublish), 2*i); - } - }, - displayError = helper.displayError, - closeAndFinish = helper.closeAndFinish, - monitorConnection = helper.monitorConnection, - bestTransport = helper.bestTransport; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); +'use strict'; + +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var expect = chai.expect; + var rest; + var publishIntervalHelper = function (currentMessageNum, channel, dataFn, onPublish) { + return function (currentMessageNum) { + channel.publish('event0', dataFn(), function () { + onPublish(); + }); + }; }; - - exports.setupUpgradeRest = function(test) { - test.expect(1); - rest = helper.AblyRest(); - test.ok(true, 'rest client set up'); - test.done(); + var publishAtIntervals = function (numMessages, channel, dataFn, onPublish) { + for (var i = numMessages; i > 0; i--) { + setTimeout(publishIntervalHelper(i, channel, dataFn, onPublish), 2 * i); + } }; + var closeAndFinish = helper.closeAndFinish; + var monitorConnection = helper.monitorConnection; + var bestTransport = helper.bestTransport; - /* - * Publish once with REST, before upgrade, verify message received - */ - exports.publishpreupgrade = function(test) { - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - /* connect and attach */ - realtime.connection.on('connected', function() { - //console.log('publishpreupgrade: connected'); - var testMsg = 'Hello world'; - var rtChannel = realtime.channels.get('publishpreupgrade'); - rtChannel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + helper.displayError(err)); - closeAndFinish(test, realtime); + if (bestTransport === 'web_socket') { + describe('realtime/upgrade', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); return; } - - /* subscribe to event */ - rtChannel.subscribe('event0', function(msg) { - test.expect(2); - test.ok(true, 'Received event0'); - test.equal(msg.data, testMsg, 'Unexpected msg text received'); - closeAndFinish(test, realtime); - }); - - /* publish event */ - var restChannel = rest.channels.get('publishpreupgrade'); - restChannel.publish('event0', testMsg, function(err) { - if(err) { - test.ok(false, 'Publish failed with error: ' + helper.displayError(err)); - closeAndFinish(test, realtime); - } - }); + rest = helper.AblyRest(); + done(); }); }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - /* - * Publish once with REST, after upgrade, verify message received on active transport - */ - exports.publishpostupgrade0 = function(test) { - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - - /* subscribe to event */ - var rtChannel = realtime.channels.get('publishpostupgrade0'); - rtChannel.subscribe('event0', function(msg) { - test.expect(2); - test.ok(true, 'Received event0'); - test.equal(msg.data, testMsg, 'Unexpected msg text received'); - var closeFn = function() { - closeAndFinish(test, realtime); - }; - if (isBrowser) - setTimeout(closeFn, 0); - else - process.nextTick(closeFn); - }); - - /* publish event */ - var testMsg = 'Hello world'; - var restChannel = rest.channels.get('publishpostupgrade0'); - var connectionManager = realtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - //console.log('publishpostupgrade0: transport active: transport = ' + transport); - if(transport.toString().match(/wss?\:/)) { - if(rtChannel.state == 'attached') { - //console.log('*** publishpostupgrade0: publishing (channel attached on transport active) ...'); - restChannel.publish('event0', testMsg, function(err) { - //console.log('publishpostupgrade0: publish returned err = ' + displayError(err)); - if(err) { - test.ok(false, 'Publish failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); + afterEach(helper.clearTransportPreference); + + /* + * Publish once with REST, before upgrade, verify message received + */ + it('publishpreupgrade', function (done) { + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); + /* connect and attach */ + realtime.connection.on('connected', function () { + //console.log('publishpreupgrade: connected'); + var testMsg = 'Hello world'; + var rtChannel = realtime.channels.get('publishpreupgrade'); + rtChannel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; } - }); - } else { - rtChannel.on('attached', function() { - //console.log('*** publishpostupgrade0: publishing (channel attached after wait) ...'); - restChannel.publish('event0', testMsg, function(err) { - //console.log('publishpostupgrade0: publish returned err = ' + displayError(err)); - if(err) { - test.ok(false, 'Publish failed with error: ' + displayError(err)); - closeAndFinish(test, realtime); + + /* subscribe to event */ + rtChannel.subscribe('event0', function (msg) { + try { + expect(msg.data).to.equal(testMsg, 'Unexpected msg text received'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + + /* publish event */ + var restChannel = rest.channels.get('publishpreupgrade'); + restChannel.publish('event0', testMsg, function (err) { + if (err) { + closeAndFinish(done, realtime, err); } }); }); - } + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - /* - * Publish once with REST, after upgrade, verify message not received on inactive transport - */ - exports.publishpostupgrade1 = function(test) { - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - - /* subscribe to event */ - var rtChannel = realtime.channels.get('publishpostupgrade1'); - rtChannel.subscribe('event0', function(msg) { - test.expect(2); - test.ok(true, 'Received event0'); - test.equal(msg.data, testMsg, 'Unexpected msg text received'); - var closeFn = function() { - closeAndFinish(test, realtime); - }; - if (isBrowser) - setTimeout(closeFn, 0); - else - process.nextTick(closeFn); - }); + /* + * Publish once with REST, after upgrade, verify message received on active transport + */ + it('publishpostupgrade0', function (done) { + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); - /* publish event */ - var testMsg = 'Hello world'; - var restChannel = rest.channels.get('publishpostupgrade1'); - var connectionManager = realtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - if(helper.isComet(transport)) { - /* override the processing of incoming messages on this channel - * so we can see if a message arrived. - * NOTE: this relies on knowledge of the internal implementation - * of the transport */ + /* subscribe to event */ + var rtChannel = realtime.channels.get('publishpostupgrade0'); + rtChannel.subscribe('event0', function (msg) { + try { + expect(msg.data).to.equal(testMsg, 'Unexpected msg text received'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + var closeFn = function () { + closeAndFinish(done, realtime); + }; + if (isBrowser) setTimeout(closeFn, 0); + else process.nextTick(closeFn); + }); - var originalOnProtocolMessage = transport.onProtocolMessage; - transport.onProtocolMessage = function(message) { - if(message.messages) - test.ok(false, 'Message received on comet transport'); - originalOnProtocolMessage.apply(this, arguments); - }; - } - }); - connectionManager.on('transport.active', function(transport) { - if(helper.isWebsocket(transport)) { - if(rtChannel.state == 'attached') { - //console.log('*** publishing (channel attached on transport active) ...'); - restChannel.publish('event0', testMsg); - } else { - rtChannel.on('attached', function() { - //console.log('*** publishing (channel attached after wait) ...'); - restChannel.publish('event0', testMsg); - }); - } + /* publish event */ + var testMsg = 'Hello world'; + var restChannel = rest.channels.get('publishpostupgrade0'); + var connectionManager = realtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + //console.log('publishpostupgrade0: transport active: transport = ' + transport); + if (transport.toString().match(/wss?\:/)) { + if (rtChannel.state == 'attached') { + //console.log('*** publishpostupgrade0: publishing (channel attached on transport active) ...'); + restChannel.publish('event0', testMsg, function (err) { + //console.log('publishpostupgrade0: publish returned err = ' + displayError(err)); + if (err) { + closeAndFinish(done, realtime, err); + } + }); + } else { + rtChannel.on('attached', function () { + //console.log('*** publishpostupgrade0: publishing (channel attached after wait) ...'); + restChannel.publish('event0', testMsg, function (err) { + //console.log('publishpostupgrade0: publish returned err = ' + displayError(err)); + if (err) { + closeAndFinish(done, realtime, err); + } + }); + }); + } + } + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'Channel attach failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + /* + * Publish once with REST, after upgrade, verify message not received on inactive transport + */ + it('publishpostupgrade1', function (done) { + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); - /** - * Publish and subscribe, text protocol - */ - exports.upgradepublish0 = function(test) { - var count = 10; - var cbCount = 10; - var checkFinish = function() { - if(count <= 0 && cbCount <= 0) { - closeAndFinish(test, realtime); - } - }; - var onPublish = function() { - --cbCount; - checkFinish(); - }; - var transportOpts = {useBinaryProtocol: false, transports: helper.availableTransports}; - var realtime = helper.AblyRealtime(transportOpts); - test.expect(count); - var channel = realtime.channels.get('upgradepublish0'); - /* subscribe to event */ - channel.subscribe('event0', function() { - test.ok(true, 'Received event0'); - --count; - checkFinish(); - }, function() { - var dataFn = function() { return 'Hello world at: ' + new Date() }; - publishAtIntervals(count, channel, dataFn, onPublish); - }); - }; + /* subscribe to event */ + var rtChannel = realtime.channels.get('publishpostupgrade1'); + rtChannel.subscribe('event0', function (msg) { + try { + expect(msg.data).to.equal(testMsg, 'Unexpected msg text received'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + var closeFn = function () { + closeAndFinish(done, realtime); + }; + if (isBrowser) setTimeout(closeFn, 0); + else process.nextTick(closeFn); + }); - /** - * Publish and subscribe, binary protocol - */ - exports.upgradepublish1 = function(test) { - var count = 10; - var cbCount = 10; - var checkFinish = function() { - if(count <= 0 && cbCount <= 0) { - closeAndFinish(test, realtime); - } - }; - var onPublish = function() { - --cbCount; - checkFinish(); - }; - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - var realtime = helper.AblyRealtime(transportOpts); - test.expect(count); - var channel = realtime.channels.get('upgradepublish1'); - /* subscribe to event */ - channel.subscribe('event0', function() { - test.ok(true, 'Received event0'); - --count; - checkFinish(); - }, function() { - var dataFn = function() { return 'Hello world at: ' + new Date() }; - publishAtIntervals(count, channel, dataFn, onPublish); - }); - }; + /* publish event */ + var testMsg = 'Hello world'; + var restChannel = rest.channels.get('publishpostupgrade1'); + var connectionManager = realtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + if (helper.isComet(transport)) { + /* override the processing of incoming messages on this channel + * so we can see if a message arrived. + * NOTE: this relies on knowledge of the internal implementation + * of the transport */ + + var originalOnProtocolMessage = transport.onProtocolMessage; + transport.onProtocolMessage = function (message) { + if (message.messages) { + closeAndFinish(done, realtime, new Error('Message received on comet transport')); + return; + } + originalOnProtocolMessage.apply(this, arguments); + }; + } + }); + connectionManager.on('transport.active', function (transport) { + if (helper.isWebsocket(transport)) { + if (rtChannel.state == 'attached') { + //console.log('*** publishing (channel attached on transport active) ...'); + restChannel.publish('event0', testMsg); + } else { + rtChannel.on('attached', function () { + //console.log('*** publishing (channel attached after wait) ...'); + restChannel.publish('event0', testMsg); + }); + } + } + }); - /* - * Base upgrade case - */ - exports.upgradebase0 = function(test) { - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - test.expect(2); - try { - var realtime = helper.AblyRealtime(transportOpts); - /* check that we see the transport we're interested in get activated, - * and that we see the comet transport deactivated */ - var failTimer = setTimeout(function() { - test.ok(false, 'upgrade heartbeat failed (timer expired)'); - closeAndFinish(test, realtime); - }, 120000); - - var connectionManager = realtime.connection.connectionManager; - connectionManager.once('transport.inactive', function(transport) { - if(transport.toString().indexOf('/comet/') > -1) - test.ok(true, 'verify comet transport deactivated'); - }); - connectionManager.on('transport.active', function(transport) { - if(transport.toString().match(/wss?\:/)) { - clearTimeout(failTimer); - var closeFn = function() { - test.ok(true, 'verify upgrade to ws transport'); - closeAndFinish(test, realtime); - }; - if (isBrowser) - setTimeout(closeFn, 0); - else - process.nextTick(closeFn); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'upgrade connect with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - /* - * Check active heartbeat, text protocol - */ - exports.upgradeheartbeat0 = function(test) { - var transportOpts = {useBinaryProtocol: false, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - - /* when we see the transport we're interested in get activated, - * listen for the heartbeat event */ - var failTimer; - var connectionManager = realtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - if(transport.toString().match(/wss?\:/)) - transport.on('heartbeat', function() { - transport.off('heartbeat'); - clearTimeout(failTimer); - test.ok(true, 'verify upgrade heartbeat'); - closeAndFinish(test, realtime); - }); - transport.ping(); + /** + * Publish and subscribe, text protocol + */ + it('upgradepublish0', function (done) { + var count = 10; + var cbCount = 10; + var checkFinish = function () { + if (count <= 0 && cbCount <= 0) { + closeAndFinish(done, realtime); + } + }; + var onPublish = function () { + --cbCount; + checkFinish(); + }; + var transportOpts = { useBinaryProtocol: false, transports: helper.availableTransports }; + var realtime = helper.AblyRealtime(transportOpts); + var channel = realtime.channels.get('upgradepublish0'); + /* subscribe to event */ + channel.subscribe( + 'event0', + function () { + --count; + checkFinish(); + }, + function () { + var dataFn = function () { + return 'Hello world at: ' + new Date(); + }; + publishAtIntervals(count, channel, dataFn, onPublish); + } + ); }); - realtime.connection.on('connected', function() { - failTimer = setTimeout(function() { - test.ok(false, 'upgrade heartbeat failed (timer expired)'); - closeAndFinish(test, realtime); - }, 120000); + /** + * Publish and subscribe, binary protocol + */ + it('upgradepublish1', function (done) { + var count = 10; + var cbCount = 10; + var checkFinish = function () { + if (count <= 0 && cbCount <= 0) { + closeAndFinish(done, realtime); + } + }; + var onPublish = function () { + --cbCount; + checkFinish(); + }; + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + var realtime = helper.AblyRealtime(transportOpts); + var channel = realtime.channels.get('upgradepublish1'); + /* subscribe to event */ + channel.subscribe( + 'event0', + function () { + --count; + checkFinish(); + }, + function () { + var dataFn = function () { + return 'Hello world at: ' + new Date(); + }; + publishAtIntervals(count, channel, dataFn, onPublish); + } + ); }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'upgrade connect with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - /* - * Check active heartbeat, binary protocol - */ - exports.upgradeheartbeat1 = function(test) { - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - - /* when we see the transport we're interested in get activated, - * listen for the heartbeat event */ - var failTimer; - var connectionManager = realtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - if(transport.toString().match(/wss?\:/)) - transport.on('heartbeat', function() { - transport.off('heartbeat'); - clearTimeout(failTimer); - test.ok(true, 'verify upgrade heartbeat'); - closeAndFinish(test, realtime); + /* + * Base upgrade case + */ + it('upgradebase0', function (done) { + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + var cometDeactivated = false; + try { + var realtime = helper.AblyRealtime(transportOpts); + /* check that we see the transport we're interested in get activated, + * and that we see the comet transport deactivated */ + var failTimer = setTimeout(function () { + closeAndFinish(done, realtime, new Error('upgrade heartbeat failed (timer expired)')); + }, 120000); + + var connectionManager = realtime.connection.connectionManager; + connectionManager.once('transport.inactive', function (transport) { + if (transport.toString().indexOf('/comet/') > -1) cometDeactivated = true; + }); + connectionManager.on('transport.active', function (transport) { + if (transport.toString().match(/wss?\:/)) { + clearTimeout(failTimer); + var closeFn = function () { + try { + expect(cometDeactivated).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }; + if (isBrowser) { + setTimeout(closeFn, 0); + } else { + process.nextTick(closeFn); + } + } }); - transport.ping(); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } }); - realtime.connection.on('connected', function() { - failTimer = setTimeout(function() { - test.ok(false, 'upgrade heartbeat failed (timer expired)'); - closeAndFinish(test, realtime); - }, 120000); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'upgrade connect with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + /* + * Check active heartbeat, text protocol + */ + it('upgradeheartbeat0', function (done) { + var transportOpts = { useBinaryProtocol: false, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); + + /* when we see the transport we're interested in get activated, + * listen for the heartbeat event */ + var failTimer; + var connectionManager = realtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + if (transport.toString().match(/wss?\:/)) + transport.on('heartbeat', function () { + transport.off('heartbeat'); + clearTimeout(failTimer); + closeAndFinish(done, realtime); + }); + transport.ping(); + }); - /* - * Check heartbeat does not fire on inactive transport, text protocol - */ - exports.upgradeheartbeat2 = function(test) { - var transportOpts = {useBinaryProtocol: false, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - - /* when we see the transport we're interested in get activated, - * listen for the heartbeat event */ - var failTimer, cometTransport, wsTransport; - var connectionManager = realtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - var transportDescription = transport.toString(); - //console.log('active transport: ' + transportDescription); - if(transportDescription.indexOf('/comet/') > -1) { - cometTransport = transport; - cometTransport.on('heartbeat', function () { - test.ok(false, 'verify heartbeat does not fire on inactive transport'); - closeAndFinish(test, realtime); + realtime.connection.on('connected', function () { + failTimer = setTimeout(function () { + closeAndFinish(done, realtime, new Error('upgrade heartbeat failed (timer expired)')); + }, 120000); }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } - if(transportDescription.match(/wss?\:/)) { - wsTransport = transport; - wsTransport.on('heartbeat', function () { - clearTimeout(failTimer); - /* wait a couple of seconds to give it time - * in case it might still fire */ - test.ok(true, 'verify upgrade heartbeat'); - setTimeout(function () { - closeAndFinish(test, realtime); - }, 2000); + }); + + /* + * Check active heartbeat, binary protocol + */ + it('upgradeheartbeat1', function (done) { + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); + + /* when we see the transport we're interested in get activated, + * listen for the heartbeat event */ + var failTimer; + var connectionManager = realtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + if (transport.toString().match(/wss?\:/)) + transport.on('heartbeat', function () { + transport.off('heartbeat'); + clearTimeout(failTimer); + closeAndFinish(done, realtime); + }); + transport.ping(); + }); + + realtime.connection.on('connected', function () { + failTimer = setTimeout(function () { + closeAndFinish(done, realtime, new Error('upgrade heartbeat failed (timer expired)')); + }, 120000); }); - wsTransport.ping(); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } }); - realtime.connection.on('connected', function() { - failTimer = setTimeout(function() { - test.ok(false, 'upgrade heartbeat failed (timer expired)'); - closeAndFinish(test, realtime); - }, 120000); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'upgrade connect with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + /* + * Check heartbeat does not fire on inactive transport, text protocol + */ + it('upgradeheartbeat2', function (done) { + var transportOpts = { useBinaryProtocol: false, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); + + /* when we see the transport we're interested in get activated, + * listen for the heartbeat event */ + var failTimer, cometTransport, wsTransport; + var connectionManager = realtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + var transportDescription = transport.toString(); + //console.log('active transport: ' + transportDescription); + if (transportDescription.indexOf('/comet/') > -1) { + cometTransport = transport; + cometTransport.on('heartbeat', function () { + closeAndFinish(done, realtime, new Error('verify heartbeat does not fire on inactive transport')); + }); + } + if (transportDescription.match(/wss?\:/)) { + wsTransport = transport; + wsTransport.on('heartbeat', function () { + clearTimeout(failTimer); + /* wait a couple of seconds to give it time + * in case it might still fire */ + setTimeout(function () { + closeAndFinish(done, realtime); + }, 2000); + }); + wsTransport.ping(); + } + }); - /* - * Check heartbeat does not fire on inactive transport, binary protocol - */ - exports.upgradeheartbeat3 = function(test) { - var transportOpts = {useBinaryProtocol: true, transports: helper.availableTransports}; - test.expect(1); - try { - var realtime = helper.AblyRealtime(transportOpts); - - /* when we see the transport we're interested in get activated, - * listen for the heartbeat event */ - var failTimer, cometTransport, wsTransport; - var connectionManager = realtime.connection.connectionManager; - connectionManager.on('transport.active', function(transport) { - var transportDescription = transport.toString(); - //console.log('active transport: ' + transportDescription); - if(transportDescription.indexOf('/comet/') > -1) { - cometTransport = transport; - cometTransport.on('heartbeat', function () { - test.ok(false, 'verify heartbeat does not fire on inactive transport'); - closeAndFinish(test, realtime); + realtime.connection.on('connected', function () { + failTimer = setTimeout(function () { + closeAndFinish(done, realtime, new Error('upgrade heartbeat failed (timer expired)')); + }, 120000); }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } - if(transportDescription.match(/wss?\:/)) { - wsTransport = transport; - wsTransport.on('heartbeat', function () { - clearTimeout(failTimer); - /* wait a couple of seconds to give it time - * in case it might still fire */ - test.ok(true, 'verify upgrade heartbeat'); - setTimeout(function() { - closeAndFinish(test, realtime); - }, 2000); + }); + + /* + * Check heartbeat does not fire on inactive transport, binary protocol + */ + it('upgradeheartbeat3', function (done) { + var transportOpts = { useBinaryProtocol: true, transports: helper.availableTransports }; + try { + var realtime = helper.AblyRealtime(transportOpts); + + /* when we see the transport we're interested in get activated, + * listen for the heartbeat event */ + var failTimer, cometTransport, wsTransport; + var connectionManager = realtime.connection.connectionManager; + connectionManager.on('transport.active', function (transport) { + var transportDescription = transport.toString(); + //console.log('active transport: ' + transportDescription); + if (transportDescription.indexOf('/comet/') > -1) { + cometTransport = transport; + cometTransport.on('heartbeat', function () { + closeAndFinish(done, realtime, new Error('verify heartbeat does not fire on inactive transport')); + }); + } + if (transportDescription.match(/wss?\:/)) { + wsTransport = transport; + wsTransport.on('heartbeat', function () { + clearTimeout(failTimer); + /* wait a couple of seconds to give it time + * in case it might still fire */ + setTimeout(function () { + closeAndFinish(done, realtime); + }, 2000); + }); + wsTransport.ping(); + } }); - wsTransport.ping(); + + realtime.connection.on('connected', function () { + failTimer = setTimeout(function () { + closeAndFinish(done, realtime, new Error('upgrade heartbeat failed (timer expired)')); + }, 120000); + }); + monitorConnection(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); } }); - realtime.connection.on('connected', function() { - failTimer = setTimeout(function() { - test.ok(false, 'upgrade heartbeat failed (timer expired)'); - closeAndFinish(test, realtime); - }, 120000); - }); - monitorConnection(test, realtime); - } catch(e) { - test.ok(false, 'upgrade connect with key failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; + it('unrecoverableUpgrade', function (done) { + var realtime, + fakeConnectionKey = '_____!ablyjs_test_fake-key____', + fakeConnectionId = 'ablyjs_tes'; + + try { + /* on base transport active */ + realtime = helper.AblyRealtime({ transports: helper.availableTransports }); + realtime.connection.connectionManager.once('transport.active', function (transport) { + expect( + transport.toString().indexOf('/comet/') > -1, + 'assert first transport to become active is a comet transport' + ).to.be.ok; + try { + expect(realtime.connection.errorReason).to.equal(null, 'Check connection.errorReason is initially null'); + /* sabotage the upgrade */ + realtime.connection.connectionManager.connectionKey = fakeConnectionKey; + realtime.connection.connectionManager.connectionId = fakeConnectionId; + } catch (err) { + closeAndFinish(done, realtiem, err); + return; + } - exports.unrecoverableUpgrade = function(test) { - test.expect(7); - var realtime, - fakeConnectionKey = '_____!ablyjs_test_fake-key____', - fakeConnectionId = 'ablyjs_tes'; - - try { - /* on base transport active */ - realtime = helper.AblyRealtime({transports: helper.availableTransports}); - realtime.connection.connectionManager.once('transport.active', function(transport) { - test.ok(transport.toString().indexOf('/comet/') > -1, 'assert first transport to become active is a comet transport'); - test.equal(realtime.connection.errorReason, null, 'Check connection.errorReason is initially null'); - /* sabotage the upgrade */ - realtime.connection.connectionManager.connectionKey = fakeConnectionKey; - realtime.connection.connectionManager.connectionId = fakeConnectionId; - - /* on upgrade failure */ - realtime.connection.once('update', function(stateChange) { - test.equal(stateChange.reason.code, 80008, 'Check correct (unrecoverable connection) error'); - test.equal(stateChange.current, 'connected', 'Check current is connected'); - test.equal(realtime.connection.errorReason.code, 80008, 'Check error set in connection.errorReason'); - test.equal(realtime.connection.state, 'connected', 'Check still connected'); - - /* Check events not still paused */ - var channel = realtime.channels.get('unrecoverableUpgrade'); - channel.attach(function(err) { - if(err) { test.ok(false, 'Attach error ' + helper.displayError(err)); } - channel.subscribe(function(msg) { - test.ok(true, 'Successfully received message'); - closeAndFinish(test, realtime); + /* on upgrade failure */ + realtime.connection.once('update', function (stateChange) { + try { + expect(stateChange.reason.code).to.equal(80008, 'Check correct (unrecoverable connection) error'); + expect(stateChange.current).to.equal('connected', 'Check current is connected'); + expect(realtime.connection.errorReason.code).to.equal( + 80008, + 'Check error set in connection.errorReason' + ); + expect(realtime.connection.state).to.equal('connected', 'Check still connected'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + /* Check events not still paused */ + var channel = realtime.channels.get('unrecoverableUpgrade'); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + channel.subscribe(function (msg) { + closeAndFinish(done, realtime); + }); + channel.publish('msg', null); + }); }); - channel.publish('msg', null); }); - }); + } catch (err) { + closeAndFinish(done, realtime, err); + } }); - } catch(e) { - test.ok(false, 'test failed with exception: ' + e.stack); - closeAndFinish(test, realtime); - } - }; - /* - * Check that a message that fails to publish on a comet transport can be - * seamlessly transferred to the websocket transport and published there - */ - exports.message_timeout_stalling_upgrade = function(test) { - var realtime = helper.AblyRealtime({transports: helper.availableTransports, httpRequestTimeout: 3000}), - channel = realtime.channels.get('timeout1'), - connectionManager = realtime.connection.connectionManager; - - realtime.connection.once('connected', function() { - channel.attach(function(err) { - if(err) { - test.ok(false, 'Attach failed with error: ' + helper.displayError(err)); - closeAndFinish(test, realtime); - return; - } - /* Sabotage comet sending */ - var transport = connectionManager.activeProtocol.getTransport(); - test.ok(helper.isComet(transport), 'Check active transport is still comet'); - transport.sendUri = helper.unroutableAddress; - - async.parallel([ - function(cb) { - channel.subscribe('event', function() { - test.ok(true, 'Received message'); - cb(); - }); - }, - function(cb) { - channel.publish('event', null, function(err) { - test.ok(!err, 'Successfully published message'); - cb(); - }); - } - ], function() { - closeAndFinish(test, realtime); + /* + * Check that a message that fails to publish on a comet transport can be + * seamlessly transferred to the websocket transport and published there + */ + it('message_timeout_stalling_upgrade', function (done) { + var realtime = helper.AblyRealtime({ transports: helper.availableTransports, httpRequestTimeout: 3000 }), + channel = realtime.channels.get('timeout1'), + connectionManager = realtime.connection.connectionManager; + + realtime.connection.once('connected', function () { + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + /* Sabotage comet sending */ + var transport = connectionManager.activeProtocol.getTransport(); + try { + expect(helper.isComet(transport), 'Check active transport is still comet').to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + transport.sendUri = helper.unroutableAddress; + + async.parallel( + [ + function (cb) { + channel.subscribe('event', function () { + cb(); + }); + }, + function (cb) { + channel.publish('event', null, function (err) { + try { + expect(!err, 'Successfully published message').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }); }); }); - }); - }; - /* - * Check that a lack of response to an upgrade sync doesn't stall things forever - */ - exports.unresponsive_upgrade_sync = function(test) { - test.expect(5); - var realtime = helper.AblyRealtime({transports: helper.availableTransports, realtimeRequestTimeout: 2000}), - connection = realtime.connection; - - connection.connectionManager.on('transport.pending', function(transport) { - if(!helper.isWebsocket(transport)) return; - - var originalOnProtocolMessage = transport.onProtocolMessage; - /* Stub out sync message one time */ - transport.onProtocolMessage = function(message) { - if(message.action === 16) { - connection.connectionManager.off('transport.pending'); - test.ok(true, 'sync received'); - transport.onProtocolMessage = originalOnProtocolMessage; - } else { - originalOnProtocolMessage.call(transport, message); - } - }; - }); + /* + * Check that a lack of response to an upgrade sync doesn't stall things forever + */ + it('unresponsive_upgrade_sync', function (done) { + var realtime = helper.AblyRealtime({ transports: helper.availableTransports, realtimeRequestTimeout: 2000 }), + connection = realtime.connection; + + connection.connectionManager.on('transport.pending', function (transport) { + if (!helper.isWebsocket(transport)) return; + + var originalOnProtocolMessage = transport.onProtocolMessage; + /* Stub out sync message one time */ + transport.onProtocolMessage = function (message) { + if (message.action === 16) { + connection.connectionManager.off('transport.pending'); + transport.onProtocolMessage = originalOnProtocolMessage; + } else { + originalOnProtocolMessage.call(transport, message); + } + }; + }); - connection.once('connected', function() { - test.ok(true, 'First connected'); - connection.once('disconnected', function() { - test.ok(true, 'After sync times out, disconnected'); - connection.once('connected', function() { - test.ok(true, 'Connect again'); - if(helper.isWebsocket(connection.connectionManager.activeProtocol.getTransport())) { - /* Must be running multiple tests at once and another one set the transport preference */ - test.ok(true, 'Upgrade not tried again because of test parallelism'); - closeAndFinish(test, realtime); - } else { - connection.connectionManager.on('transport.active', function(transport) { - if(helper.isWebsocket(transport)) { - test.ok(true, 'Check upgrade is tried again, and this time, succeeds'); - closeAndFinish(test, realtime); + connection.once('connected', function () { + connection.once('disconnected', function () { + connection.once('connected', function () { + if (helper.isWebsocket(connection.connectionManager.activeProtocol.getTransport())) { + /* Must be running multiple tests at once and another one set the transport preference */ + closeAndFinish(done, realtime); + } else { + connection.connectionManager.on('transport.active', function (transport) { + if (helper.isWebsocket(transport)) { + closeAndFinish(done, realtime); + } + }); } }); - } + }); }); }); - }); - }; - /* - * Check that after a successful upgrade, the transport pref is persisted, - * and subsequent connections do not upgrade - */ - exports.persist_transport_prefs = function(test) { - var realtime = helper.AblyRealtime({transports: helper.availableTransports}), - connection = realtime.connection, - connectionManager = connection.connectionManager; - - async.series([ - function(cb) { - connectionManager.once('transport.active', function(transport) { - test.ok(helper.isComet(transport), 'Check first transport to become active is comet'); - cb(); - }); - }, - function(cb) { - connectionManager.once('transport.active', function(transport) { - test.ok(helper.isWebsocket(transport), 'Check second transport to become active is ws'); - cb(); - }); - }, - function(cb) { - connection.once('closed', function() { - test.ok(true, 'closed'); - cb(); - }); - helper.Utils.nextTick(function() { - connection.close(); - }); - }, - function(cb) { - connectionManager.once('transport.active', function(transport) { - test.ok(helper.isWebsocket(transport), 'Check first transport to become active the second time round is websocket'); - cb(); - }); - connection.connect(); - } - ], function() { - closeAndFinish(test, realtime); - }); - }; + /* + * Check that after a successful upgrade, the transport pref is persisted, + * and subsequent connections do not upgrade + */ + it('persist_transport_prefs', function (done) { + var realtime = helper.AblyRealtime({ transports: helper.availableTransports }), + connection = realtime.connection, + connectionManager = connection.connectionManager; + + async.series( + [ + function (cb) { + connectionManager.once('transport.active', function (transport) { + try { + expect(helper.isComet(transport), 'Check first transport to become active is comet').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + }, + function (cb) { + connectionManager.once('transport.active', function (transport) { + try { + expect(helper.isWebsocket(transport), 'Check second transport to become active is ws').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + }, + function (cb) { + connection.once('closed', function () { + cb(); + }); + helper.Utils.nextTick(function () { + connection.close(); + }); + }, + function (cb) { + connectionManager.once('transport.active', function (transport) { + try { + expect( + helper.isWebsocket(transport), + 'Check first transport to become active the second time round is websocket' + ).to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + connection.connect(); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }); - /* - * Check that upgrades succeed even if the original transport dies before the sync - */ - exports.upgrade_original_transport_dies = function(test) { - var realtime = helper.AblyRealtime({transports: helper.availableTransports}), - connection = realtime.connection, - connectionManager = connection.connectionManager; - - async.series([ - function(cb) { - connectionManager.once('transport.active', function(transport) { - test.ok(helper.isComet(transport), 'Check first transport to become active is comet'); - cb(); - }); - }, - function(cb) { - connectionManager.on('transport.pending', function(transport) { - if(!helper.isWebsocket(transport)) return; // in browser, might be xhr_streaming - connectionManager.off('transport.pending'); - /* Abort comet transport nonfatally */ - var baseTransport = connectionManager.activeProtocol.getTransport(); - test.ok(helper.isComet(baseTransport), 'Check original transport is still comet'); - /* Check that if we do get a statechange, it's to connecting, not disconnected. */ - var stateChangeListener = function(stateChange) { - test.equal(stateChange.current, 'connecting', 'check that deactivateTransport only drops us to connecting as another transport is ready for activation'); - }; - connection.once(stateChangeListener); - connectionManager.once('connectiondetails', function() { - connection.off(stateChangeListener); - /* Check the upgrade completed */ - var newActiveTransport = connectionManager.activeProtocol.getTransport(); - test.equal(transport, newActiveTransport, 'Check the upgrade transport is now active'); - cb(); - }) - transport.once('connected', function() { - baseTransport.disconnect({code: 50000, statusCode: 500, message: "a non-fatal transport error"}); - }); - }); - } - ], function() { - closeAndFinish(test, realtime); + /* + * Check that upgrades succeed even if the original transport dies before the sync + */ + it('upgrade_original_transport_dies', function (done) { + var realtime = helper.AblyRealtime({ transports: helper.availableTransports }), + connection = realtime.connection, + connectionManager = connection.connectionManager; + + async.series( + [ + function (cb) { + connectionManager.once('transport.active', function (transport) { + try { + expect(helper.isComet(transport), 'Check first transport to become active is comet').to.be.ok; + } catch (err) { + cb(err); + return; + } + cb(); + }); + }, + function (cb) { + connectionManager.on('transport.pending', function (transport) { + if (!helper.isWebsocket(transport)) return; // in browser, might be xhr_streaming + connectionManager.off('transport.pending'); + /* Abort comet transport nonfatally */ + var baseTransport = connectionManager.activeProtocol.getTransport(); + try { + expect(helper.isComet(baseTransport), 'Check original transport is still comet').to.be.ok; + } catch (err) { + cb(err); + return; + } + /* Check that if we do get a statechange, it's to connecting, not disconnected. */ + var stateChangeListener = function (stateChange) { + try { + expect(stateChange.current).to.equal( + 'connecting', + 'check that deactivateTransport only drops us to connecting as another transport is ready for activation' + ); + } catch (err) { + cb(err); + } + }; + connection.once(stateChangeListener); + connectionManager.once('connectiondetails', function () { + connection.off(stateChangeListener); + /* Check the upgrade completed */ + var newActiveTransport = connectionManager.activeProtocol.getTransport(); + try { + expect(transport).to.equal(newActiveTransport, 'Check the upgrade transport is now active'); + } catch (err) { + cb(err); + return; + } + cb(); + }); + transport.once('connected', function () { + baseTransport.disconnect({ code: 50000, statusCode: 500, message: 'a non-fatal transport error' }); + }); + }); + } + ], + function (err) { + closeAndFinish(done, realtime, err); + } + ); + }); }); - }; - - if (bestTransport === 'web_socket') { - helper.withMocha('realtime/upgrade', exports); } -}); \ No newline at end of file +}); diff --git a/spec/rest/auth.test.js b/spec/rest/auth.test.js index c873f924c5..5052616e9e 100644 --- a/spec/rest/auth.test.js +++ b/spec/rest/auth.test.js @@ -1,733 +1,725 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async', 'globals'], function(Ably, helper, async, globals) { - var currentTime, rest, exports = {}, - _exports = {}, - utils = helper.Utils, - echoServer = 'https://echo.ably.io'; - - var getServerTime = function(callback) { - rest.time(function(err, time) { - if(err) { callback(err); } - callback(null, time); +'use strict'; + +define(['chai', 'shared_helper', 'async', 'globals'], function (chai, helper, async, globals) { + var currentTime; + var rest; + var expect = chai.expect; + var utils = helper.Utils; + var echoServer = 'https://echo.ably.io'; + + describe('rest/auth', function () { + this.timeout(60 * 1000); + + var getServerTime = function (callback) { + rest.time(function (err, time) { + if (err) { + callback(err); + } + callback(null, time); + }); + }; + + before(function (done) { + helper.setupApp(function () { + rest = helper.AblyRest({ queryTime: true }); + getServerTime(function (err, time) { + if (err) { + done(err); + return; + } + currentTime = time; + expect(true, 'Obtained time').to.be.ok; + done(); + }); + }); }); - }; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function() { - rest = helper.AblyRest({queryTime: true}); - getServerTime(function(err, time) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); + + it('Base token generation case', function (done) { + rest.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); return; } - currentTime = time; - test.ok(true, 'Obtained time'); - test.done(); + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(tokenDetails.expires).to.equal(60 * 60 * 1000 + tokenDetails.issued, 'Verify default expiry period'); + expect(JSON.parse(tokenDetails.capability)).to.deep.equal({ '*': ['*'] }, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } }); }); - }; - - /* - * Base token generation case - */ - exports.authbase0 = function(test) { - test.expect(1); - rest.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(5); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.equal(tokenDetails.expires, 60*60*1000 + tokenDetails.issued, 'Verify default expiry period'); - test.deepEqual(JSON.parse(tokenDetails.capability), {'*':['*']}, 'Verify token capability'); - test.done(); - }); - }; - - /* - * Base token generation with options - */ - exports.authbase1 = function(test) { - test.expect(1); - rest.auth.requestToken(null, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(4); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.deepEqual(JSON.parse(tokenDetails.capability), {'*':['*']}, 'Verify token capability'); - test.done(); - }); - }; - - /* - * Generate token and init library with it - */ - exports.authbase2 = function(test) { - test.expect(1); - rest.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.ok((tokenDetails.token), 'Verify token value'); - try { - var restInit = helper.AblyRest({ token: tokenDetails.token }); - test.done(); - } catch(e) { - test.ok(false, helper.displayError(e)); - test.done(); - } - }); - }; - - /* - * Token generation with explicit timestamp - */ - exports.authtime0 = function(test) { - test.expect(1); - getServerTime(function(err, serverTime) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - rest.auth.requestToken({timestamp: serverTime}, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); + it('Base token generation with options', function (done) { + rest.auth.requestToken(null, function (err, tokenDetails) { + if (err) { + done(err); return; } - test.expect(4); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.deepEqual(JSON.parse(tokenDetails.capability), {'*':['*']}, 'Verify token capability'); - test.done(); + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(JSON.parse(tokenDetails.capability)).to.deep.equal({ '*': ['*'] }, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } }); }); - }; - - /* - * Token generation with explicit timestamp (invalid) - */ - exports.authtime1 = function(test) { - test.expect(1); - var badTime = utils.now() - 30*60*1000; - rest.auth.requestToken({timestamp:badTime}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 401, 'Verify token request rejected with bad timestamp'); - test.done(); - return; - } - test.ok(false, 'Invalid timestamp, expected rejection'); - test.done(); - }); - }; - - /* - * Token generation with system timestamp - */ - exports.authtime2 = function(test) { - test.expect(1); - rest.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(4); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.deepEqual(JSON.parse(tokenDetails.capability), {'*':['*']}, 'Verify token capability'); - test.done(); + + it('Generate token and init library with it', function (done) { + rest.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + helper.AblyRest({ token: tokenDetails.token }); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * Token generation with duplicate nonce - */ - exports.authnonce0 = function(test) { - test.expect(1); - getServerTime(function(err, serverTime) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - rest.auth.requestToken({timestamp:serverTime, nonce:'1234567890123456'}, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); + it('Token generation with explicit timestamp', function (done) { + getServerTime(function (err, serverTime) { + if (err) { + done(err); return; } - rest.auth.requestToken({timestamp:serverTime, nonce:'1234567890123456'}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 401, 'Verify request rejected with duplicated nonce'); - test.done(); + + rest.auth.requestToken({ timestamp: serverTime }, function (err, tokenDetails) { + if (err) { + done(err); return; } - test.ok(false, 'Invalid nonce, expected rejection'); - test.done(); + try { + expect(tokenDetails.token).to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(JSON.parse(tokenDetails.capability)).to.deep.equal({ '*': ['*'] }, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } }); }); }); - }; - - /* - * Token generation with clientId - */ - exports.authclientid0 = function(test) { - test.expect(1); - var testClientId = 'test client id'; - rest.auth.requestToken({clientId:testClientId}, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(5); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.equal(tokenDetails.clientId, testClientId, 'Verify client id'); - test.deepEqual(JSON.parse(tokenDetails.capability), {'*':['*']}, 'Verify token capability'); - test.done(); - }); - }; - - /* - * Token generation with empty string clientId should error - */ - exports.authemptyclientid = function(test) { - test.expect(1); - rest.auth.requestToken({clientId: ''}, function(err, tokenDetails) { - if(err) { - test.equal(err.code, 40012); - test.done(); - return; - } - test.ok(false); - test.done(); - }); - }; - - /* - * Token generation with capability that subsets key capability - */ - exports.authcapability0 = function(test) { - test.expect(1); - var testCapability = {onlythischannel:['subscribe']}; - rest.auth.requestToken({capability:testCapability}, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(4); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.deepEqual(JSON.parse(tokenDetails.capability), testCapability, 'Verify token capability'); - test.done(); + + it('Token generation with invalid timestamp', function (done) { + var badTime = utils.now() - 30 * 60 * 1000; + rest.auth.requestToken({ timestamp: badTime }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(401, 'Verify token request rejected with bad timestamp'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Invalid timestamp, expected rejection')); + }); }); - }; - - /* - * Token generation with specified key - */ - exports.authkey0 = function(test) { - test.expect(1); - var testKeyOpts = {key: helper.getTestApp().keys[1].keyStr}; - var testCapability = JSON.parse(helper.getTestApp().keys[1].capability); - rest.auth.requestToken(null, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(5); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.equal(tokenDetails.keyName, helper.getTestApp().keys[1].keyName, 'Verify token key'); - test.deepEqual(JSON.parse(tokenDetails.capability), testCapability, 'Verify token capability'); - test.done(); + + it('Token generation with system timestamp', function (done) { + rest.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(JSON.parse(tokenDetails.capability)).to.deep.equal({ '*': ['*'] }, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * Token generation with explicit auth - */ - exports.authexplicit_simple = function(test) { - test.expect(1); - rest.auth.getAuthHeaders(function(err, authHeaders) { - rest.auth.authOptions.requestHeaders = authHeaders; - rest.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); + + it('Token generation with duplicate nonce', function (done) { + getServerTime(function (err, serverTime) { + if (err) { + done(err); return; } - test.expect(4); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.equal(tokenDetails.keyName, helper.getTestApp().keys[0].keyName, 'Verify token key'); - test.done(); + rest.auth.requestToken({ timestamp: serverTime, nonce: '1234567890123456' }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + rest.auth.requestToken({ timestamp: serverTime, nonce: '1234567890123456' }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(401, 'Verify request rejected with duplicated nonce'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Invalid nonce, expected rejection')); + }); + }); }); }); - }; - - /* - * Token generation with explicit auth, different key - */ - exports.authexplicit_key = function(test) { - test.expect(1); - rest.auth.getAuthHeaders(function(err, authHeaders) { - var testKeyOpts = {key: helper.getTestApp().keys[1].keyStr}; - var testCapability = JSON.parse(helper.getTestApp().keys[1].capability); - rest.auth.requestToken(null, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); + + it('Token generation with clientId', function (done) { + var testClientId = 'test client id'; + rest.auth.requestToken({ clientId: testClientId }, function (err, tokenDetails) { + if (err) { + done(err); return; } - test.expect(5); - test.ok((tokenDetails.token), 'Verify token value'); - test.ok((tokenDetails.issued && tokenDetails.issued >= currentTime), 'Verify token issued'); - test.ok((tokenDetails.expires && tokenDetails.expires > tokenDetails.issued), 'Verify token expires'); - test.equal(tokenDetails.keyName, helper.getTestApp().keys[1].keyName, 'Verify token key'); - test.deepEqual(JSON.parse(tokenDetails.capability), testCapability, 'Verify token capability'); - test.done(); + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(tokenDetails.clientId).to.equal(testClientId, 'Verify client id'); + expect(JSON.parse(tokenDetails.capability)).to.deep.equal({ '*': ['*'] }, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } }); }); - }; - - /* - * Token generation with invalid mac - */ - exports.authmac0 = function(test) { - test.expect(1); - rest.auth.requestToken({mac: '12345'}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 401, 'Verify request rejected with bad mac'); - test.done(); - return; - } - test.ok(false, 'Invalid mac, expected rejection'); - test.done(); + + it('Token generation with empty string clientId should error', function (done) { + rest.auth.requestToken({ clientId: '' }, function (err, tokenDetails) { + if (err) { + expect(err.code).to.equal(40012); + done(); + return; + } + done(new Error('Expected token generation to error with empty string clientId')); + }); }); - }; - - /* - * Token generation with defaultTokenParams set and no tokenParams passed in - */ - exports.authdefaulttokenparams0 = function(test) { - test.expect(1); - var rest1 = helper.AblyRest({defaultTokenParams: {ttl: 123, clientId: "foo"}}); - rest1.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.expect(3); - test.ok((tokenDetails.token), 'Verify token value'); - test.equal(tokenDetails.clientId, 'foo', 'Verify client id from defaultTokenParams used'); - test.equal(tokenDetails.expires - tokenDetails.issued, 123, 'Verify ttl from defaultTokenParams used'); - test.done(); + + it('Token generation with capability that subsets key capability', function (done) { + var testCapability = { onlythischannel: ['subscribe'] }; + rest.auth.requestToken({ capability: testCapability }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(testCapability, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * Token generation: if tokenParams passed in, defaultTokenParams should be ignored altogether, not merged - */ - exports.authdefaulttokenparams1 = function(test) { - test.expect(2); - var rest1 = helper.AblyRest({defaultTokenParams: {ttl: 123, clientId: "foo"}}); - rest1.auth.requestToken({clientId: 'bar'}, null, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.equal(tokenDetails.clientId, 'bar', 'Verify client id passed in is used, not the one from defaultTokenParams'); - test.equal(tokenDetails.expires - tokenDetails.issued, 60 * 60 * 1000, 'Verify ttl from defaultTokenParams ignored completely, even though not overridden'); - test.done(); + + it('Token generation with specified key', function (done) { + var testKeyOpts = { key: helper.getTestApp().keys[1].keyStr }; + var testCapability = JSON.parse(helper.getTestApp().keys[1].capability); + rest.auth.requestToken(null, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(tokenDetails.keyName).to.equal(helper.getTestApp().keys[1].keyName, 'Verify token key'); + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(testCapability, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * authorize with different args - */ - exports.authauthorize = function(test) { - test.expect(3); - async.parallel([ - function(cb) { - rest.auth.authorize(null, null, function(err, tokenDetails) { - test.ok(tokenDetails.token, 'Check token obtained'); - cb(err); - }); - }, - function(cb) { - rest.auth.authorize(null, function(err, tokenDetails) { - test.ok(tokenDetails.token, 'Check token obtained'); - cb(err); - }); - }, - function(cb) { - rest.auth.authorize(function(err, tokenDetails) { - test.ok(tokenDetails.token, 'Check token obtained'); - cb(err); + + it('Token generation with explicit auth', function (done) { + rest.auth.getAuthHeaders(function (err, authHeaders) { + if (err) { + done(err); + return; + } + rest.auth.authOptions.requestHeaders = authHeaders; + rest.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(tokenDetails.keyName).to.equal(helper.getTestApp().keys[0].keyName, 'Verify token key'); + done(); + } catch (err) { + done(err); + } }); - } - ], function(err) { - if(err) test.ok(false, "authorize returned an error: " + helper.displayError(err)); - test.done(); + }); }); - }; - - /* - * Specify non-default ttl - */ - exports.authttl0 = function(test) { - test.expect(1); - rest.auth.requestToken({ttl:100*1000}, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.equal(tokenDetails.expires, 100*1000 + tokenDetails.issued, 'Verify non-default expiry period'); - test.done(); + + it('Token generation with explicit auth, different key', function (done) { + rest.auth.getAuthHeaders(function (err, authHeaders) { + var testKeyOpts = { key: helper.getTestApp().keys[1].keyStr }; + var testCapability = JSON.parse(helper.getTestApp().keys[1].capability); + rest.auth.requestToken(null, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.issued && tokenDetails.issued >= currentTime, 'Verify token issued').to.be.ok; + expect(tokenDetails.expires && tokenDetails.expires > tokenDetails.issued, 'Verify token expires').to.be.ok; + expect(tokenDetails.keyName).to.equal(helper.getTestApp().keys[1].keyName, 'Verify token key'); + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(testCapability, 'Verify token capability'); + done(); + } catch (e) { + done(e); + } + }); + }); }); - }; - - /* - * Excessive ttl - */ - exports.authttl1 = function(test) { - test.expect(1); - rest.auth.requestToken({ttl: 365*24*60*60*1000}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 400, 'Verify request rejected with excessive expiry'); - test.done(); - return; - } - test.ok(false, 'Excessive expiry, expected rejection'); - test.done(); + + it('Token generation with invalid mac', function (done) { + rest.auth.requestToken({ mac: '12345' }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(401, 'Verify request rejected with bad mac'); + done(); + } catch (e) { + done(e); + } + return; + } + done(new Error('Invalid mac, expected rejection')); + }); }); - }; - - /* - * Negative ttl - */ - exports.authttl2 = function(test) { - test.expect(1); - rest.auth.requestToken({ttl: -1}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 400, 'Verify request rejected with negative expiry'); - test.done(); - return; - } - test.ok(false, 'Negative expiry, expected rejection'); - test.done(); + + it('Token generation with defaultTokenParams set and no tokenParams passed in', function (done) { + var rest1 = helper.AblyRest({ defaultTokenParams: { ttl: 123, clientId: 'foo' } }); + rest1.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.token, 'Verify token value').to.be.ok; + expect(tokenDetails.clientId).to.equal('foo', 'Verify client id from defaultTokenParams used'); + expect(tokenDetails.expires - tokenDetails.issued).to.equal(123, 'Verify ttl from defaultTokenParams used'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * Invalid ttl - */ - exports.authttl3 = function(test) { - test.expect(1); - rest.auth.requestToken({ttl: 'notanumber'}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 400, 'Verify request rejected with invalid expiry'); - test.done(); - return; - } - test.ok(false, 'Invalid expiry, expected rejection'); - test.done(); + + it('Token generation: if tokenParams passed in, defaultTokenParams should be ignored altogether, not merged', function (done) { + var rest1 = helper.AblyRest({ defaultTokenParams: { ttl: 123, clientId: 'foo' } }); + rest1.auth.requestToken({ clientId: 'bar' }, null, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.clientId).to.equal( + 'bar', + 'Verify clientId passed in is used, not the one from defaultTokenParams' + ); + expect(tokenDetails.expires - tokenDetails.issued).to.equal( + 60 * 60 * 1000, + 'Verify ttl from defaultTokenParams ignored completely, even though not overridden' + ); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * createTokenRequest uses the key it was initialized with if authOptions is null, - * and the token request includes all the fields it should include, but - * doesn't include ttl or capability by default - */ - exports.auth_createTokenRequest_given_key = function(test) { - test.expect(6); - rest.auth.createTokenRequest(null, null, function(err, tokenRequest) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.ok('mac' in tokenRequest, 'check tokenRequest contains a mac'); - test.ok('nonce' in tokenRequest, 'check tokenRequest contains a nonce'); - test.ok('timestamp' in tokenRequest, 'check tokenRequest contains a timestamp'); - test.ok(!('ttl' in tokenRequest), 'check tokenRequest does not contains a ttl by default'); - test.ok(!('capability' in tokenRequest), 'check tokenRequest does not contains capabilities by default'); - test.equal(tokenRequest.keyName, helper.getTestApp().keys[0].keyName); - test.done(); + + /* + * authorize with different args + */ + it('Authorize with different args', function (done) { + async.parallel( + [ + function (cb) { + rest.auth.authorize(null, null, function (err, tokenDetails) { + expect(tokenDetails.token, 'Check token obtained').to.be.ok; + cb(err); + }); + }, + function (cb) { + rest.auth.authorize(null, function (err, tokenDetails) { + expect(tokenDetails.token, 'Check token obtained').to.be.ok; + cb(err); + }); + }, + function (cb) { + rest.auth.authorize(function (err, tokenDetails) { + expect(tokenDetails.token, 'Check token obtained').to.be.ok; + cb(err); + }); + } + ], + function (err) { + if (err) { + done(err); + } + done(); + } + ); }); - }; - - /* - * createTokenRequest: no authoptions, callback as 2nd param - */ - exports.auth_createTokenRequest_params0 = function(test) { - test.expect(1); - rest.auth.createTokenRequest(null, function(err, tokenRequest) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.equal(tokenRequest.keyName, helper.getTestApp().keys[0].keyName); - test.done(); + + it('Specify non-default ttl', function (done) { + rest.auth.requestToken({ ttl: 100 * 1000 }, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(tokenDetails.expires).to.equal(100 * 1000 + tokenDetails.issued, 'Verify non-default expiry period'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* - * createTokenRequest: no authoptions or tokenparams, callback as 1st param - */ - exports.auth_createTokenRequest_params1 = function(test) { - test.expect(1); - rest.auth.createTokenRequest(function(err, tokenRequest) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.equal(tokenRequest.keyName, helper.getTestApp().keys[0].keyName); - test.done(); + + it('Should error with excessive ttl', function (done) { + rest.auth.requestToken({ ttl: 365 * 24 * 60 * 60 * 1000 }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(400, 'Verify request rejected with excessive expiry'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Excessive expiry, expected rejection')); + }); }); - }; - - /* - * createTokenRequest uses the key it was initialized with if authOptions does not have a "key" key - */ - exports.auth_createTokenRequest_given_key2 = function(test) { - test.expect(1); - rest.auth.createTokenRequest(function(err, tokenRequest) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.equal(tokenRequest.keyName, helper.getTestApp().keys[0].keyName); - test.done(); + + it('Should error with negative ttl', function (done) { + rest.auth.requestToken({ ttl: -1 }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(400, 'Verify request rejected with negative expiry'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Negative expiry, expected rejection')); + }); }); - }; - - /* - * createTokenRequest given capability object JSON-stringifies it - */ - exports.auth_createTokenRequest_capability_object = function(test) { - test.expect(1); - var capability = {'*':['*']}; - rest.auth.createTokenRequest({capability: capability}, null, function(err, tokenRequest) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenRequest.capability), capability, 'Verify createTokenRequest has JSON-stringified capability'); - test.done(); + + it('Should error with invalid ttl', function (done) { + rest.auth.requestToken({ ttl: 'notanumber' }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(400, 'Verify request rejected with invalid expiry'); + done(); + } catch (e) { + done(e); + } + return; + } + done(new Error('Invalid expiry, expected rejection')); + }); }); - }; - - /* RSC1, RSC1a, RSC1c, RSA4f, RSA8c, RSA3d - * Tests the different combinations of authParams declared above, with valid keys - */ - exports.rest_jwt = testJWTAuthParams({}); - exports.rest_jwt_with_jwt_return_type = testJWTAuthParams({returnType: 'jwt'}); - /* The embedded tests rely on the echoserver getting a token from realtime, so won't work against a local realtime */ - if(globals.environment !== 'local') { - exports.rest_jwt_embedded = testJWTAuthParams({jwtType: 'embedded', environment: globals.environment}); - exports.rest_jwt_embedded_encrypted = testJWTAuthParams({jwtType: 'embedded', environment: globals.environment}); - } - - function testJWTAuthParams(params) { return function(test) { - test.expect(1); - var currentKey = helper.getTestApp().keys[0]; - var keys = {keyName: currentKey.keyName, keySecret: currentKey.keySecret}; - var authParams = utils.mixin(keys, params); - var authUrl = echoServer + '/createJWT' + utils.toQueryString(authParams); - var restJWTRequester = helper.AblyRest({authUrl: authUrl}); - - restJWTRequester.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, err.message); - test.done(); - return; - } - var restClient = helper.AblyRest({token: tokenDetails.token}); - restClient.stats(function(err, stats) { - if(err) { - test.ok(false, err.message); - test.done(); + + /* + * createTokenRequest uses the key it was initialized with if authOptions is null, + * and the token request includes all the fields it should include, but + * doesn't include ttl or capability by default + */ + it('createTokenRequest without authOptions', function (done) { + rest.auth.createTokenRequest(null, null, function (err, tokenRequest) { + if (err) { + done(err); return; } - test.ok(true, 'Verify that stats request succeeded'); - test.done(); - }); - }) - }}; - - /* - * Tests JWT request with invalid keys - */ - exports.rest_jwt_with_invalid_keys = function(test) { - test.expect(2); - var keys = {keyName: 'invalid.invalid', keySecret: 'invalidinvalid'}; - var authUrl = echoServer + '/createJWT' + utils.toQueryString(keys); - var restJWTRequester = helper.AblyRest({authUrl: authUrl}); - - restJWTRequester.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, err.message); - test.done(); - return; - } - var restClient = helper.AblyRest({token: tokenDetails.token}); - restClient.stats(function(err, stats) { - test.equal(err.code, 40400, 'Verify token is invalid because app id does not exist'); - test.equal(err.statusCode, 404, 'Verify token is invalid because app id does not exist'); - test.done(); + try { + expect('mac' in tokenRequest, 'check tokenRequest contains a mac').to.be.ok; + expect('nonce' in tokenRequest, 'check tokenRequest contains a nonce').to.be.ok; + expect('timestamp' in tokenRequest, 'check tokenRequest contains a timestamp').to.be.ok; + expect(!('ttl' in tokenRequest), 'check tokenRequest does not contains a ttl by default').to.be.ok; + expect( + !('capability' in tokenRequest), + 'check tokenRequest does not contains capabilities by default' + ).to.be.ok; + expect(tokenRequest.keyName).to.equal(helper.getTestApp().keys[0].keyName); + done(); + } catch (err) { + done(err); + } }); }); - }; - - /* RSA8g - * Tests JWT with authCallback - */ - exports.rest_jwt_with_authCallback = function(test) { - test.expect(2); - var currentKey = helper.getTestApp().keys[0]; - var keys = {keyName: currentKey.keyName, keySecret: currentKey.keySecret}; - var authUrl = echoServer + '/createJWT' + utils.toQueryString(keys); - var restJWTRequester = helper.AblyRest({authUrl: authUrl}); - - var authCallback = function(tokenParams, callback) { - restJWTRequester.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, err.message); - test.done(); + + it('createTokenRequest without authOptions, callback as 2nd param', function (done) { + rest.auth.createTokenRequest(null, function (err, tokenRequest) { + if (err) { + done(err); return; } - callback(null, tokenDetails.token); + try { + expect(tokenRequest.keyName).to.equal(helper.getTestApp().keys[0].keyName); + done(); + } catch (err) { + done(err); + } }); - }; + }); - var restClient = helper.AblyRest({ authCallback: authCallback }); - restClient.stats(function(err, stats) { - if(err) { - test.ok(false, err.message); - test.done(); - return; - } - test.equal(err, null, 'Verify that the error is null'); - test.ok(true, 'Verify that stats request succeeded'); - test.done(); + it('createTokenRequest without authOptions or tokenParams, callback as 1st param', function (done) { + rest.auth.createTokenRequest(function (err, tokenRequest) { + if (err) { + done(err); + return; + } + try { + expect(tokenRequest.keyName).to.equal(helper.getTestApp().keys[0].keyName); + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* RSA8g - * Tests JWT with authCallback and invalid keys - */ - exports.rest_jwt_with_authCallback_and_invalid_keys = function(test) { - test.expect(2); - var keys = {keyName: 'invalid.invalid', keySecret: 'invalidinvalid'}; - var authUrl = echoServer + '/createJWT' + utils.toQueryString(keys); - var restJWTRequester = helper.AblyRest({authUrl: authUrl}); - - var authCallback = function(tokenParams, callback) { - restJWTRequester.auth.requestToken(function(err, tokenDetails) { - if(err) { - test.ok(false, err.message); - test.done(); + + it('createTokenRequest uses the key it was initialized with if authOptions does not have a "key" key', function (done) { + rest.auth.createTokenRequest(function (err, tokenRequest) { + if (err) { + done(err); return; } - callback(null, tokenDetails.token); + try { + expect(tokenRequest.keyName).to.equal(helper.getTestApp().keys[0].keyName); + done(); + } catch (err) { + done(err); + } }); - }; + }); - var restClient = helper.AblyRest({ authCallback: authCallback }); - restClient.stats(function(err, stats) { - test.equal(err.code, 40400, 'Verify code is 40400'); - test.equal(err.statusCode, 404, 'Verify token is invalid because app id does not exist'); - test.done(); + it('createTokenRequest should serialise capability object as JSON', function (done) { + var capability = { '*': ['*'] }; + rest.auth.createTokenRequest({ capability: capability }, null, function (err, tokenRequest) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenRequest.capability)).to.deep.equal( + capability, + 'Verify createTokenRequest has JSON-stringified capability' + ); + done(); + } catch (err) { + done(err); + } + }); }); - }; - exports.auth_concurrent = function(test) { - var authCallbackInvocations = 0; - function authCallback(tokenParams, callback) { - authCallbackInvocations++; - rest.auth.createTokenRequest(tokenParams, callback); + /** + * Creates a test fixture which checks that the rest client can succesfully make a stats request with the given authParams. + * @param {string} description Mocha test description + * @param {object} params The authParams to be tested + */ + function testJWTAuthParams(description, params) { + it(description, function (done) { + var currentKey = helper.getTestApp().keys[0]; + var keys = { keyName: currentKey.keyName, keySecret: currentKey.keySecret }; + var authParams = utils.mixin(keys, params); + var authUrl = echoServer + '/createJWT' + utils.toQueryString(authParams); + var restJWTRequester = helper.AblyRest({ authUrl: authUrl }); + + restJWTRequester.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + var restClient = helper.AblyRest({ token: tokenDetails.token }); + restClient.stats(function (err, stats) { + if (err) { + done(err); + return; + } + done(); + }); + }); + }); + } + + /* RSC1, RSC1a, RSC1c, RSA4f, RSA8c, RSA3d + * Tests the different combinations of authParams declared above, with valid keys + */ + testJWTAuthParams('Basic rest JWT', {}); + testJWTAuthParams('Rest JWT with return type ', { returnType: 'jwt' }); + /* The embedded tests rely on the echoserver getting a token from realtime, so won't work against a local realtime */ + if (globals.environment !== 'local') { + testJWTAuthParams('Rest embedded JWT', { jwtType: 'embedded', environment: globals.environment }); + testJWTAuthParams('Rest embedded JWT with encryption', { + jwtType: 'embedded', + environment: globals.environment, + encrypted: 1 + }); } - /* Example client-side using the token */ - var restClient = helper.AblyRest({ authCallback: authCallback }); - var channel = restClient.channels.get('auth_concurrent'); - - async.parallel([ - channel.history.bind(channel), - channel.history.bind(channel) - ], function(err) { - test.ok(!err, err && helper.displayError(err)); - test.equal(authCallbackInvocations, 1, 'Check authCallback only invoked once -- was: ' + authCallbackInvocations) - test.done(); - return; + it('JWT request with invalid key', function (done) { + var keys = { keyName: 'invalid.invalid', keySecret: 'invalidinvalid' }; + var authUrl = echoServer + '/createJWT' + utils.toQueryString(keys); + var restJWTRequester = helper.AblyRest({ authUrl: authUrl }); + + restJWTRequester.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + var restClient = helper.AblyRest({ token: tokenDetails.token }); + restClient.stats(function (err, stats) { + try { + expect(err.code).to.equal(40400, 'Verify token is invalid because app id does not exist'); + expect(err.statusCode).to.equal(404, 'Verify token is invalid because app id does not exist'); + done(); + return; + } catch (err) { + done(err); + } + }); + }); }); - }; - exports.auth_promises = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; - } - test.expect(6); - var rest = helper.AblyRest({promises: true}); - - var promise1 = rest.auth.requestToken(); - var promise2 = rest.auth.requestToken({ttl: 100}); - var promise3 = rest.auth.requestToken({ttl: 100}, {key: helper.getTestApp().keys[1].keyStr}); - var promise4 = rest.auth.createTokenRequest(); - var promise5 = rest.auth.createTokenRequest({ttl: 100}); - var promise6 = rest.auth.requestToken({ttl: 100}, {key: 'bad'})['catch'](function(err) { - test.ok(true, 'Token attempt with bad key was rejected') + /* + * RSA8g + */ + it('Rest JWT with authCallback', function (done) { + var currentKey = helper.getTestApp().keys[0]; + var keys = { keyName: currentKey.keyName, keySecret: currentKey.keySecret }; + var authUrl = echoServer + '/createJWT' + utils.toQueryString(keys); + var restJWTRequester = helper.AblyRest({ authUrl: authUrl }); + + var authCallback = function (tokenParams, callback) { + restJWTRequester.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + callback(null, tokenDetails.token); + }); + }; + + var restClient = helper.AblyRest({ authCallback: authCallback }); + restClient.stats(function (err, stats) { + if (err) { + done(err); + return; + } + try { + expect(err).to.equal(null, 'Verify that the error is null'); + done(); + } catch (err) { + done(err); + } + }); + }); + + /* + * RSA8g + */ + it('Rest JWT with authCallback and invalid keys', function (done) { + var keys = { keyName: 'invalid.invalid', keySecret: 'invalidinvalid' }; + var authUrl = echoServer + '/createJWT' + utils.toQueryString(keys); + var restJWTRequester = helper.AblyRest({ authUrl: authUrl }); + + var authCallback = function (tokenParams, callback) { + restJWTRequester.auth.requestToken(function (err, tokenDetails) { + if (err) { + done(err); + return; + } + callback(null, tokenDetails.token); + }); + }; + + var restClient = helper.AblyRest({ authCallback: authCallback }); + restClient.stats(function (err, stats) { + try { + expect(err.code).to.equal(40400, 'Verify code is 40400'); + expect(err.statusCode).to.equal(404, 'Verify token is invalid because app id does not exist'); + done(); + } catch (err) { + done(err); + } + }); }); - Promise.all([promise1, promise2, promise3, promise4, promise5, promise6]).then(function(results) { - for(var i=0; i<5; i++) { - test.ok(results[i].token || results[i].nonce) + it('authCallback is only invoked once on concurrent auth', function (done) { + var authCallbackInvocations = 0; + function authCallback(tokenParams, callback) { + authCallbackInvocations++; + rest.auth.createTokenRequest(tokenParams, callback); } - test.done(); - })['catch'](function(err) { - test.ok(false, 'a token request failed with error: ' + displayError(err)); - test.done(); + + /* Example client-side using the token */ + var restClient = helper.AblyRest({ authCallback: authCallback }); + var channel = restClient.channels.get('auth_concurrent'); + + async.parallel([channel.history.bind(channel), channel.history.bind(channel)], function (err) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(authCallbackInvocations).to.equal( + 1, + 'Check authCallback only invoked once -- was: ' + authCallbackInvocations + ); + done(); + } catch (err) { + done(err); + } + }); }); - }; - helper.withMocha('rest/auth', exports); -}); \ No newline at end of file + if (typeof Promise !== 'undefined') { + it('Promise based auth', function (done) { + var rest = helper.AblyRest({ promises: true }); + + var promise1 = rest.auth.requestToken(); + var promise2 = rest.auth.requestToken({ ttl: 100 }); + var promise3 = rest.auth.requestToken({ ttl: 100 }, { key: helper.getTestApp().keys[1].keyStr }); + var promise4 = rest.auth.createTokenRequest(); + var promise5 = rest.auth.createTokenRequest({ ttl: 100 }); + var promise6 = rest.auth.requestToken({ ttl: 100 }, { key: 'bad' })['catch'](function (err) { + expect(true, 'Token attempt with bad key was rejected').to.be.ok; + }); + + Promise.all([promise1, promise2, promise3, promise4, promise5, promise6]) + .then(function (results) { + try { + for (var i = 0; i < 5; i++) { + expect(results[i].token || results[i].nonce).to.be.ok; + } + done(); + } catch (err) { + done(err); + } + }) + ['catch'](function (err) { + done(err); + }); + }); + } + }); +}); diff --git a/spec/rest/bufferutils.test.js b/spec/rest/bufferutils.test.js index 6e4ca210aa..926afe4ba1 100644 --- a/spec/rest/bufferutils.test.js +++ b/spec/rest/bufferutils.test.js @@ -1,60 +1,63 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}; +define(['ably', 'chai'], function (Ably, chai) { + var expect = chai.expect; var BufferUtils = Ably.Realtime.BufferUtils; var testString = 'test'; var testBase64 = 'dGVzdA=='; var testHex = '74657374'; - function isWordArray(ob) { return ob !== null && ob !== undefined && ob.sigBytes !== undefined; } + function isWordArray(ob) { + return ob !== null && ob !== undefined && ob.sigBytes !== undefined; + } - exports.bufferutils_encodedecode = function(test) { - /* base64 */ - test.equal(BufferUtils.base64Encode(BufferUtils.utf8Encode(testString)), testBase64); - test.equal(BufferUtils.utf8Decode(BufferUtils.base64Decode(testBase64)), testString); + describe('rest/bufferutils', function () { + it('Basic encoding and decoding', function () { + /* base64 */ + expect(BufferUtils.base64Encode(BufferUtils.utf8Encode(testString))).to.equal(testBase64); + expect(BufferUtils.utf8Decode(BufferUtils.base64Decode(testBase64))).to.equal(testString); - /* hex */ - test.equal(BufferUtils.hexEncode(BufferUtils.utf8Encode(testString)), testHex); - test.equal(BufferUtils.utf8Decode(BufferUtils.hexDecode(testHex)), testString); + /* hex */ + expect(BufferUtils.hexEncode(BufferUtils.utf8Encode(testString))).to.equal(testHex); + expect(BufferUtils.utf8Decode(BufferUtils.hexDecode(testHex))).to.equal(testString); - /* compare */ - test.equal(0, BufferUtils.bufferCompare(BufferUtils.utf8Encode(testString), BufferUtils.utf8Encode(testString))); - test.notEqual(0, BufferUtils.bufferCompare(BufferUtils.utf8Encode(testString), BufferUtils.utf8Encode('other'))); - - test.done(); - }; + /* compare */ + expect( + BufferUtils.bufferCompare(BufferUtils.utf8Encode(testString), BufferUtils.utf8Encode(testString)) + ).to.equal(0); + expect( + BufferUtils.bufferCompare(BufferUtils.utf8Encode(testString), BufferUtils.utf8Encode('other')) + ).to.not.equal(0); + }); /* In node it's idiomatic for most methods dealing with binary data to * return Buffers. In the browser it's more idiomatic to return * ArrayBuffers (or in browser too old to support ArrayBuffer, wordarrays). */ - exports.bufferutils_resulttype = function(test) { - if(typeof Buffer !== 'undefined') { - /* node */ - test.equal(BufferUtils.utf8Encode(testString).constructor, Buffer); - test.equal(BufferUtils.hexDecode(testHex).constructor, Buffer); - test.equal(BufferUtils.base64Decode(testBase64).constructor, Buffer); - test.equal(BufferUtils.toBuffer(BufferUtils.utf8Encode(testString)).constructor, Buffer); - test.equal(BufferUtils.toArrayBuffer(BufferUtils.utf8Encode(testString)).constructor, ArrayBuffer); - } else if(typeof ArrayBuffer !== 'undefined') { - /* modern browsers */ - if(typeof TextDecoder !== 'undefined') { - test.equal(BufferUtils.utf8Encode(testString).constructor, ArrayBuffer); + it('BufferUtils return correct types', function () { + if (typeof Buffer !== 'undefined') { + /* node */ + expect(BufferUtils.utf8Encode(testString).constructor).to.equal(Buffer); + expect(BufferUtils.hexDecode(testHex).constructor).to.equal(Buffer); + expect(BufferUtils.base64Decode(testBase64).constructor).to.equal(Buffer); + expect(BufferUtils.toBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(Buffer); + expect(BufferUtils.toArrayBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(ArrayBuffer); + } else if (typeof ArrayBuffer !== 'undefined') { + /* modern browsers */ + if (typeof TextDecoder !== 'undefined') { + expect(BufferUtils.utf8Encode(testString).constructor).to.equal(ArrayBuffer); + } else { + expect(isWordArray(BufferUtils.utf8Encode(testString))).to.be.ok; + } + expect(BufferUtils.hexDecode(testHex).constructor).to.equal(ArrayBuffer); + expect(BufferUtils.base64Decode(testBase64).constructor).to.equal(ArrayBuffer); + expect(BufferUtils.toBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(Uint8Array); + expect(BufferUtils.toArrayBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(ArrayBuffer); } else { - test.ok(isWordArray(BufferUtils.utf8Encode(testString))); + /* legacy browsers */ + expect(isWordArray(BufferUtils.utf8Encode(testString))).to.be.ok; + expect(isWordArray(BufferUtils.hexDecode(testHex))).to.be.ok; + expect(isWordArray(BufferUtils.base64Decode(testBase64))).to.be.ok; + expect(isWordArray(BufferUtils.toWordArray(BufferUtils.utf8Encode(testString)))).to.be.ok; } - test.equal(BufferUtils.hexDecode(testHex).constructor, ArrayBuffer); - test.equal(BufferUtils.base64Decode(testBase64).constructor, ArrayBuffer); - test.equal(BufferUtils.toBuffer(BufferUtils.utf8Encode(testString)).constructor, Uint8Array); - test.equal(BufferUtils.toArrayBuffer(BufferUtils.utf8Encode(testString)).constructor, ArrayBuffer); - } else { - /* legacy browsers */ - test.ok(isWordArray(BufferUtils.utf8Encode(testString))); - test.ok(isWordArray(BufferUtils.hexDecode(testHex))); - test.ok(isWordArray(BufferUtils.base64Decode(testBase64))); - test.ok(isWordArray(BufferUtils.toWordArray(BufferUtils.utf8Encode(testString)))); - } - test.done(); - }; - - helper.withMocha('rest/bufferutils', exports); + }); + }); }); diff --git a/spec/rest/capability.test.js b/spec/rest/capability.test.js index 324dcfba31..c8b750f363 100644 --- a/spec/rest/capability.test.js +++ b/spec/rest/capability.test.js @@ -1,277 +1,284 @@ -"use strict"; - -define(['ably', 'shared_helper'], function(Ably, helper) { - var currentTime, rest, testApp, exports = {}; +'use strict'; +define(['shared_helper', 'chai'], function (helper, chai) { + var currentTime; + var rest; + var testApp; + var expect = chai.expect; var invalid0 = { - channel0:['publish_'] - }; - + channel0: ['publish_'] + }; var invalid1 = { - channel0:['*', 'publish'] - }; - + channel0: ['*', 'publish'] + }; var invalid2 = { - channel0:[] - }; + channel0: [] + }; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } + describe('rest/capability', function () { + this.timeout(60 * 1000); - rest = helper.AblyRest(); - testApp = helper.getTestApp(); - rest.time(function(err, time) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); return; } - currentTime = time; - test.ok(true, 'Obtained time'); - test.done(); + + rest = helper.AblyRest(); + testApp = helper.getTestApp(); + rest.time(function (err, time) { + if (err) { + done(err); + return; + } + currentTime = time; + done(); + }); }); }); - }; - /* - * Blanket intersection with specified key - */ - exports.authcapability0 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[1].keyStr}; - var testCapability = JSON.parse(testApp.keys[1].capability); - rest.auth.requestToken(null, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), testCapability, 'Verify token capability'); - test.done(); + it('Blanket intersection with specified key', function (done) { + var testKeyOpts = { key: testApp.keys[1].keyStr }; + var testCapability = JSON.parse(testApp.keys[1].capability); + rest.auth.requestToken(null, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(testCapability, 'Verify token capability'); + done(); + } catch (e) { + done(e); + } + }); }); - }; - /* - * Equal intersection with specified key - */ - exports.authcapability1 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[1].keyStr}; - var testCapability = JSON.parse(testApp.keys[1].capability); - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), testCapability, 'Verify token capability'); - test.done(); + it('Equal intersection with specified key', function (done) { + var testKeyOpts = { key: testApp.keys[1].keyStr }; + var testCapability = JSON.parse(testApp.keys[1].capability); + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(testCapability, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - /* - * Empty ops intersection - */ - exports.authcapability2 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[1].keyStr}; - var testCapability = {"canpublish:test":['subscribe']}; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 401, 'Verify request rejected with insufficient capability'); - test.done(); - return; - } - test.ok(false, 'Invalid capability, expected rejection'); - test.done(); + it('Empty ops intersection', function (done) { + var testKeyOpts = { key: testApp.keys[1].keyStr }; + var testCapability = { 'canpublish:test': ['subscribe'] }; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(401, 'Verify request rejected with insufficient capability'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Invalid capability, expected rejection')); + }); }); - }; - /* - * Empty paths intersection - */ - exports.authcapability3 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[2].keyStr}; - var testCapability = {channelx:['publish']}; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 401, 'Verify request rejected with insufficient capability'); - test.done(); - return; - } - test.ok(false, 'Invalid capability, expected rejection'); - test.done(); + it('Empty paths intersection', function (done) { + var testKeyOpts = { key: testApp.keys[2].keyStr }; + var testCapability = { channelx: ['publish'] }; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(401, 'Verify request rejected with insufficient capability'); + done(); + } catch (err) { + done(err); + } + return; + } + done('Invalid capability, expected rejection'); + }); }); - }; - /* - * Ops intersection non-empty - */ - exports.authcapability4 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[2].keyStr}; - var testCapability = {channel2:['presence', 'subscribe']}; - var expectedIntersection = {channel2:['subscribe']}; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); + it('Ops intersection non-empty', function (done) { + var testKeyOpts = { key: testApp.keys[2].keyStr }; + var testCapability = { channel2: ['presence', 'subscribe'] }; + var expectedIntersection = { channel2: ['subscribe'] }; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - /* - * Paths intersection non-empty - */ - exports.authcapability5 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[2].keyStr}; - var testCapability = { - channel2:['presence', 'subscribe'], - channelx:['presence', 'subscribe'] - }; - var expectedIntersection = {channel2:['subscribe']}; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); + it('Paths intersection non-empty', function (done) { + var testKeyOpts = { key: testApp.keys[2].keyStr }; + var testCapability = { + channel2: ['presence', 'subscribe'], + channelx: ['presence', 'subscribe'] + }; + var expectedIntersection = { channel2: ['subscribe'] }; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - /* - * Ops wildcard matching - */ - exports.authcapability6 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[2].keyStr}; - var testCapability = {channel2:['*']}; - var expectedIntersection = {channel2:['publish', 'subscribe']}; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); - }); - }; - exports.authcapability7 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[2].keyStr}; - var testCapability = {channel6:['publish', 'subscribe']}; - var expectedIntersection = {channel6:['publish', 'subscribe']}; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); + it('Wildcard token with publish and subscribe key', function (done) { + var testKeyOpts = { key: testApp.keys[2].keyStr }; + var testCapability = { channel2: ['*'] }; + var expectedIntersection = { channel2: ['publish', 'subscribe'] }; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - /* - * Resources wildcard matching - */ - exports.authcapability8 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[3].keyStr}; - var testCapability = {cansubscribe:['subscribe']}; - var expectedIntersection = testCapability; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); + it('Publish and subscribe token with wildcard key', function (done) { + var testKeyOpts = { key: testApp.keys[2].keyStr }; + var testCapability = { channel6: ['publish', 'subscribe'] }; + var expectedIntersection = { channel6: ['publish', 'subscribe'] }; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - exports.authcapability9 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[1].keyStr}; - var testCapability = {'canpublish:check':['publish']}; - var expectedIntersection = testCapability; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); + + it('Resources wildcard matching 1', function (done) { + var testKeyOpts = { key: testApp.keys[3].keyStr }; + var testCapability = { cansubscribe: ['subscribe'] }; + var expectedIntersection = testCapability; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - exports.authcapability10 = function(test) { - test.expect(1); - var testKeyOpts = {key: testApp.keys[3].keyStr}; - var testCapability = {'cansubscribe:*':['subscribe']}; - var expectedIntersection = testCapability; - rest.auth.requestToken({capability: testCapability}, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.deepEqual(JSON.parse(tokenDetails.capability), expectedIntersection, 'Verify token capability'); - test.done(); + + it('Resources wildcard matching 2', function (done) { + var testKeyOpts = { key: testApp.keys[1].keyStr }; + var testCapability = { 'canpublish:check': ['publish'] }; + var expectedIntersection = testCapability; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - /* Invalid capabilities */ - exports.invalid0 = function(test) { - test.expect(1); - rest.auth.requestToken({capability: invalid0}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 400, 'Verify request rejected with bad capability'); - test.done(); - return; - } - test.ok(false, 'Invalid capability, expected rejection'); - test.done(); + it('Resources wildcard matching 3', function (done) { + var testKeyOpts = { key: testApp.keys[3].keyStr }; + var testCapability = { 'cansubscribe:*': ['subscribe'] }; + var expectedIntersection = testCapability; + rest.auth.requestToken({ capability: testCapability }, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + try { + expect(JSON.parse(tokenDetails.capability)).to.deep.equal(expectedIntersection, 'Verify token capability'); + done(); + } catch (err) { + done(err); + } + }); }); - }; - exports.invalid1 = function(test) { - test.expect(1); - rest.auth.requestToken({capability: invalid1}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 400, 'Verify request rejected with bad capability'); - test.done(); - return; - } - test.ok(false, 'Invalid capability, expected rejection'); - test.done(); + + /* Invalid capabilities */ + it('Invalid capabilities 1', function (done) { + rest.auth.requestToken({ capability: invalid0 }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(400, 'Verify request rejected with bad capability'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Invalid capability, expected rejection')); + }); }); - }; - exports.invalid2 = function(test) { - test.expect(1); - rest.auth.requestToken({capability: invalid2}, function(err, tokenDetails) { - if(err) { - test.equal(err.statusCode, 400, 'Verify request rejected with bad capability'); - test.done(); - return; - } - test.ok(false, 'Invalid capability, expected rejection'); - test.done(); + + it('Invalid capabilities 2', function (done) { + rest.auth.requestToken({ capability: invalid1 }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(400, 'Verify request rejected with bad capability'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Invalid capability, expected rejection')); + }); }); - }; - helper.withMocha('rest/capability', exports); -}); \ No newline at end of file + it('Invalid capabilities 3', function (done) { + rest.auth.requestToken({ capability: invalid2 }, function (err, tokenDetails) { + if (err) { + try { + expect(err.statusCode).to.equal(400, 'Verify request rejected with bad capability'); + done(); + } catch (err) { + done(err); + } + return; + } + done(new Error('Invalid capability, expected rejection')); + }); + }); + }); +}); diff --git a/spec/rest/defaults.test.js b/spec/rest/defaults.test.js index 55950889c1..9f9bebd341 100644 --- a/spec/rest/defaults.test.js +++ b/spec/rest/defaults.test.js @@ -1,258 +1,230 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}; +define(['ably', 'chai'], function (Ably, chai) { + var expect = chai.expect; var Defaults = Ably.Rest.Defaults; - /* init with no endpoint-related options */ - exports.defaults_no_opts = function(test) { - test.expect(11); - var normalisedOptions = Defaults.normaliseOptions({}); - - test.equal(normalisedOptions.restHost, 'rest.ably.io'); - test.equal(normalisedOptions.realtimeHost, 'realtime.ably.io'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.deepEqual(normalisedOptions.fallbackHosts.sort(), Defaults.FALLBACK_HOSTS.sort()); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions).length, 4); - test.deepEqual(Defaults.getHosts(normalisedOptions)[0], normalisedOptions.restHost); - test.deepEqual(Defaults.getHost(normalisedOptions, 'rest.ably.io', false), 'rest.ably.io'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'rest.ably.io', true), 'realtime.ably.io'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with production environment */ - exports.defaults_production = function(test) { - test.expect(11); - var normalisedOptions = Defaults.normaliseOptions({environment: 'production'}); - - test.equal(normalisedOptions.restHost, 'rest.ably.io'); - test.equal(normalisedOptions.realtimeHost, 'realtime.ably.io'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.deepEqual(normalisedOptions.fallbackHosts.sort(), Defaults.FALLBACK_HOSTS.sort()); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions).length, 4); - test.deepEqual(Defaults.getHosts(normalisedOptions)[0], normalisedOptions.restHost); - test.deepEqual(Defaults.getHost(normalisedOptions, 'rest.ably.io', false), 'rest.ably.io'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'rest.ably.io', true), 'realtime.ably.io'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with given environment */ - exports.defaults_given_environment = function(test) { - test.expect(11); - var normalisedOptions = Defaults.normaliseOptions({environment: 'sandbox'}); - - test.equal(normalisedOptions.restHost, 'sandbox-rest.ably.io'); - test.equal(normalisedOptions.realtimeHost, 'sandbox-realtime.ably.io'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.deepEqual(normalisedOptions.fallbackHosts.sort(), Defaults.environmentFallbackHosts('sandbox').sort()); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions).length, 4); - test.deepEqual(Defaults.getHosts(normalisedOptions)[0], normalisedOptions.restHost); - test.deepEqual(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', false), 'sandbox-rest.ably.io'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', true), 'sandbox-realtime.ably.io'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with given environment and default fallbacks */ - /* will emit a deprecation warning */ - exports.defaults_given_environment_and_fallbackHostsUseDefault = function(test) { - test.expect(11); - var normalisedOptions = Defaults.normaliseOptions({environment: 'sandbox', fallbackHostsUseDefault: true}); - - test.equal(normalisedOptions.restHost, 'sandbox-rest.ably.io'); - test.equal(normalisedOptions.realtimeHost, 'sandbox-realtime.ably.io'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.deepEqual(normalisedOptions.fallbackHosts.sort(), Defaults.FALLBACK_HOSTS.sort()); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions).length, 4); - test.deepEqual(Defaults.getHosts(normalisedOptions)[0], normalisedOptions.restHost); - test.deepEqual(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', false), 'sandbox-rest.ably.io'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', true), 'sandbox-realtime.ably.io'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with local environment and non-default ports */ - exports.defaults_local_ports = function(test) { - test.expect(10); - var normalisedOptions = Defaults.normaliseOptions({environment: 'local', port: 8080, tlsPort: 8081}); - - test.equal(normalisedOptions.restHost, 'local-rest.ably.io'); - test.equal(normalisedOptions.realtimeHost, 'local-realtime.ably.io'); - test.equal(normalisedOptions.port, 8080); - test.equal(normalisedOptions.tlsPort, 8081); - test.equal(normalisedOptions.fallbackHosts, undefined); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions), [normalisedOptions.restHost]); - test.deepEqual(Defaults.getHost(normalisedOptions, 'local-rest.ably.io', false), 'local-rest.ably.io'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'local-rest.ably.io', true), 'local-realtime.ably.io'); - - test.equal(Defaults.getPort(normalisedOptions), 8081); - test.done(); - }; - - /* init with given host */ - exports.defaults_given_host = function(test) { - test.expect(10); - var normalisedOptions = Defaults.normaliseOptions({restHost: 'test.org'}); - - test.equal(normalisedOptions.restHost, 'test.org'); - test.equal(normalisedOptions.realtimeHost, 'test.org'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.equal(normalisedOptions.fallbackHosts, undefined); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions), [normalisedOptions.restHost]); - test.deepEqual(Defaults.getHost(normalisedOptions, 'test.org', false), 'test.org'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'test.org', true), 'test.org'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with given restHost and realtimeHost */ - exports.defaults_given_realtimehost = function(test) { - test.expect(10); - var normalisedOptions = Defaults.normaliseOptions({restHost: 'test.org', realtimeHost: 'ws.test.org'}); - - test.equal(normalisedOptions.restHost, 'test.org'); - test.equal(normalisedOptions.realtimeHost, 'ws.test.org'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.equal(normalisedOptions.fallbackHosts, undefined); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions), [normalisedOptions.restHost]); - test.deepEqual(Defaults.getHost(normalisedOptions, 'test.org', false), 'test.org'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'test.org', true), 'ws.test.org'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with given restHost and realtimeHost, using the default fallback hosts */ - exports.defaults_given_host_using_default_fallbacks = function(test) { - test.expect(3); - var normalisedOptions = Defaults.normaliseOptions({restHost: 'test.org', realtimeHost: 'ws.test.org', fallbackHostsUseDefault: true}); - - test.equal(normalisedOptions.restHost, 'test.org'); - test.equal(normalisedOptions.realtimeHost, 'ws.test.org'); - test.deepEqual(normalisedOptions.fallbackHosts.sort(), Defaults.FALLBACK_HOSTS.sort()); - test.done(); - }; - - /* init with both fallbackHosts and fallbackHostsUseDefault */ - /* will throw an error */ - exports.defaults_given_fallbackHosts_and_fallbackHostsUseDefault = function(test) { - test.expect(1); - test.throws(function() { - Defaults.normaliseOptions({fallbackHosts: ['a.example.com', 'b.example.com'], fallbackHostsUseDefault: true}); - }, "Check fallbackHosts and fallbackHostsUseDefault can't both be set"); - test.done(); - }; - - /* init with fallbackHostsUseDefault and port or tlsPort set */ - /* will throw an error */ - exports.defaults_given_fallbackHostsUseDefault_and_port_or_tlsPort = function(test) { - test.expect(2); - test.throws(function() { - Defaults.normaliseOptions({fallbackHostsUseDefault: true, port: 8080}); - }, "Check fallbackHostsUseDefault and port can't both be set"); - test.throws(function() { - Defaults.normaliseOptions({fallbackHostsUseDefault: true, tlsPort: 8081}); - }, "Check fallbackHostsUseDefault and tlsPort can't both be set"); - test.done(); - }; - - /* init with deprecated host and wsHost options */ - /* will emit a warning */ - exports.defaults_given_deprecated_host = function(test) { - test.expect(10); - var normalisedOptions = Defaults.normaliseOptions({host: 'test.org', wsHost: 'ws.test.org'}); - - test.equal(normalisedOptions.restHost, 'test.org'); - test.equal(normalisedOptions.realtimeHost, 'ws.test.org'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.equal(normalisedOptions.fallbackHosts, undefined); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions), [normalisedOptions.restHost]); - test.deepEqual(Defaults.getHost(normalisedOptions, 'test.org', false), 'test.org'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'test.org', true), 'ws.test.org'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - /* init with no endpoint-related options and given default environment */ - exports.defaults_set_default_environment = function(test) { - test.expect(11); - Defaults.ENVIRONMENT = 'sandbox'; - var normalisedOptions = Defaults.normaliseOptions({}); - - test.equal(normalisedOptions.restHost, 'sandbox-rest.ably.io'); - test.equal(normalisedOptions.realtimeHost, 'sandbox-realtime.ably.io'); - test.equal(normalisedOptions.port, 80); - test.equal(normalisedOptions.tlsPort, 443); - test.deepEqual(normalisedOptions.fallbackHosts.sort(), Defaults.environmentFallbackHosts('sandbox').sort()); - test.equal(normalisedOptions.tls, true); - - test.deepEqual(Defaults.getHosts(normalisedOptions).length, 4); - test.deepEqual(Defaults.getHosts(normalisedOptions)[0], normalisedOptions.restHost); - test.deepEqual(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', false), 'sandbox-rest.ably.io'); - test.deepEqual(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', true), 'sandbox-realtime.ably.io'); - - test.equal(Defaults.getPort(normalisedOptions), 443); - test.done(); - }; - - exports.defaults_closeOnUnload = function(test) { - test.expect(6); - var options; - - /* Default to true */ - options = Defaults.normaliseOptions({}); - test.equal(options.closeOnUnload, true); - - /* Default to false if using manual recovery */ - options = Defaults.normaliseOptions({recover: 'someRecoveryKey'}); - test.equal(options.closeOnUnload, false); - - /* Default to false if using autorecovery */ - options = Defaults.normaliseOptions({recover: function(){}}); - test.equal(options.closeOnUnload, false); - - /* can override default with manual recovery */ - options = Defaults.normaliseOptions({recover: 'someRecoveryKey', closeOnUnload: true}); - test.equal(options.closeOnUnload, true); - - /* can override default with autorecovery only at the cost of unsetting autorecovery */ - options = Defaults.normaliseOptions({recover: function(){}, closeOnUnload: true}); - test.equal(options.closeOnUnload, true); - test.ok(!options.recover); - - test.done(); - }; - - helper.withMocha('rest/defaults', exports); -}); \ No newline at end of file + describe('rest/defaults', function () { + it('Init with no endpoint-related options', function () { + var normalisedOptions = Defaults.normaliseOptions({}); + + expect(normalisedOptions.restHost).to.equal('rest.ably.io'); + expect(normalisedOptions.realtimeHost).to.equal('realtime.ably.io'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts.sort()).to.deep.equal(Defaults.FALLBACK_HOSTS.sort()); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions).length).to.equal(4); + expect(Defaults.getHosts(normalisedOptions)[0]).to.deep.equal(normalisedOptions.restHost); + expect(Defaults.getHost(normalisedOptions, 'rest.ably.io', false)).to.deep.equal('rest.ably.io'); + expect(Defaults.getHost(normalisedOptions, 'rest.ably.io', true)).to.equal('realtime.ably.io'); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + it('Init with production environment', function () { + var normalisedOptions = Defaults.normaliseOptions({ environment: 'production' }); + + expect(normalisedOptions.restHost).to.equal('rest.ably.io'); + expect(normalisedOptions.realtimeHost).to.equal('realtime.ably.io'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts.sort()).to.deep.equal(Defaults.FALLBACK_HOSTS.sort()); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions).length).to.deep.equal(4); + expect(Defaults.getHosts(normalisedOptions)[0]).to.deep.equal(normalisedOptions.restHost); + expect(Defaults.getHost(normalisedOptions, 'rest.ably.io', false)).to.deep.equal('rest.ably.io'); + expect(Defaults.getHost(normalisedOptions, 'rest.ably.io', true)).to.deep.equal('realtime.ably.io'); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + it('Init with given environment', function () { + var normalisedOptions = Defaults.normaliseOptions({ environment: 'sandbox' }); + + expect(normalisedOptions.restHost).to.equal('sandbox-rest.ably.io'); + expect(normalisedOptions.realtimeHost).to.equal('sandbox-realtime.ably.io'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts.sort()).to.deep.equal(Defaults.environmentFallbackHosts('sandbox').sort()); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions).length).to.deep.equal(4); + expect(Defaults.getHosts(normalisedOptions)[0]).to.deep.equal(normalisedOptions.restHost); + expect(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', false)).to.deep.equal('sandbox-rest.ably.io'); + expect(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', true)).to.deep.equal( + 'sandbox-realtime.ably.io' + ); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + /* will emit a deprecation warning */ + it('Init with given environment and default fallbacks', function () { + var normalisedOptions = Defaults.normaliseOptions({ environment: 'sandbox', fallbackHostsUseDefault: true }); + + expect(normalisedOptions.restHost).to.equal('sandbox-rest.ably.io'); + expect(normalisedOptions.realtimeHost).to.equal('sandbox-realtime.ably.io'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts.sort()).to.deep.equal(Defaults.FALLBACK_HOSTS.sort()); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions).length).to.deep.equal(4); + expect(Defaults.getHosts(normalisedOptions)[0]).to.deep.equal(normalisedOptions.restHost); + expect(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', false)).to.deep.equal('sandbox-rest.ably.io'); + expect(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', true)).to.deep.equal( + 'sandbox-realtime.ably.io' + ); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + it('Init with local environment and non-default ports', function () { + var normalisedOptions = Defaults.normaliseOptions({ environment: 'local', port: 8080, tlsPort: 8081 }); + + expect(normalisedOptions.restHost).to.equal('local-rest.ably.io'); + expect(normalisedOptions.realtimeHost).to.equal('local-realtime.ably.io'); + expect(normalisedOptions.port).to.equal(8080); + expect(normalisedOptions.tlsPort).to.equal(8081); + expect(normalisedOptions.fallbackHosts).to.equal(undefined); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions)).to.deep.equal([normalisedOptions.restHost]); + expect(Defaults.getHost(normalisedOptions, 'local-rest.ably.io', false)).to.deep.equal('local-rest.ably.io'); + expect(Defaults.getHost(normalisedOptions, 'local-rest.ably.io', true)).to.deep.equal('local-realtime.ably.io'); + + expect(Defaults.getPort(normalisedOptions)).to.equal(8081); + }); + + it('Init with given host', function () { + var normalisedOptions = Defaults.normaliseOptions({ restHost: 'test.org' }); + + expect(normalisedOptions.restHost).to.equal('test.org'); + expect(normalisedOptions.realtimeHost).to.equal('test.org'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts).to.equal(undefined); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions)).to.deep.equal([normalisedOptions.restHost]); + expect(Defaults.getHost(normalisedOptions, 'test.org', false)).to.deep.equal('test.org'); + expect(Defaults.getHost(normalisedOptions, 'test.org', true)).to.deep.equal('test.org'); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + /* init with given restHost and realtimeHost */ + it('Init with given restHost and realtimeHost', function () { + var normalisedOptions = Defaults.normaliseOptions({ restHost: 'test.org', realtimeHost: 'ws.test.org' }); + + expect(normalisedOptions.restHost).to.equal('test.org'); + expect(normalisedOptions.realtimeHost).to.equal('ws.test.org'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts).to.equal(undefined); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions)).to.deep.equal([normalisedOptions.restHost]); + expect(Defaults.getHost(normalisedOptions, 'test.org', false)).to.deep.equal('test.org'); + expect(Defaults.getHost(normalisedOptions, 'test.org', true)).to.deep.equal('ws.test.org'); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + /* init with given restHost and realtimeHost, using the default fallback hosts */ + it('Init with given restHost and realtimeHost, using the default fallback hosts', function () { + var normalisedOptions = Defaults.normaliseOptions({ + restHost: 'test.org', + realtimeHost: 'ws.test.org', + fallbackHostsUseDefault: true + }); + + expect(normalisedOptions.restHost).to.equal('test.org'); + expect(normalisedOptions.realtimeHost).to.equal('ws.test.org'); + expect(normalisedOptions.fallbackHosts.sort()).to.deep.equal(Defaults.FALLBACK_HOSTS.sort()); + }); + + it('Throws an error when initiated with fallbackHosts and fallbackHostsUseDefault', function () { + expect(function () { + Defaults.normaliseOptions({ fallbackHosts: ['a.example.com', 'b.example.com'], fallbackHostsUseDefault: true }); + }, "Check fallbackHosts and fallbackHostsUseDefault can't both be set").to.throw(); + }); + + it('Throws an error with initiated with fallbackHostsUseDefault and port or tlsPort set', function () { + expect(function () { + Defaults.normaliseOptions({ fallbackHostsUseDefault: true, port: 8080 }); + }, "Check fallbackHostsUseDefault and port can't both be set").to.throw; + expect(function () { + Defaults.normaliseOptions({ fallbackHostsUseDefault: true, tlsPort: 8081 }); + }, "Check fallbackHostsUseDefault and tlsPort can't both be set").to.throw; + }); + + /* will emit a warning */ + it('Init with deprecated host and wsHost options', function () { + var normalisedOptions = Defaults.normaliseOptions({ host: 'test.org', wsHost: 'ws.test.org' }); + + expect(normalisedOptions.restHost).to.equal('test.org'); + expect(normalisedOptions.realtimeHost).to.equal('ws.test.org'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts).to.equal(undefined); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions)).to.deep.equal([normalisedOptions.restHost]); + expect(Defaults.getHost(normalisedOptions, 'test.org', false)).to.deep.equal('test.org'); + expect(Defaults.getHost(normalisedOptions, 'test.org', true)).to.deep.equal('ws.test.org'); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + }); + + it('Init with no endpoint-related options and given default environment', function () { + Defaults.ENVIRONMENT = 'sandbox'; + var normalisedOptions = Defaults.normaliseOptions({}); + + expect(normalisedOptions.restHost).to.equal('sandbox-rest.ably.io'); + expect(normalisedOptions.realtimeHost).to.equal('sandbox-realtime.ably.io'); + expect(normalisedOptions.port).to.equal(80); + expect(normalisedOptions.tlsPort).to.equal(443); + expect(normalisedOptions.fallbackHosts.sort()).to.deep.equal(Defaults.environmentFallbackHosts('sandbox').sort()); + expect(normalisedOptions.tls).to.equal(true); + + expect(Defaults.getHosts(normalisedOptions).length).to.equal(4); + expect(Defaults.getHosts(normalisedOptions)[0]).to.deep.equal(normalisedOptions.restHost); + expect(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', false)).to.deep.equal('sandbox-rest.ably.io'); + expect(Defaults.getHost(normalisedOptions, 'sandbox-rest.ably.io', true)).to.deep.equal( + 'sandbox-realtime.ably.io' + ); + + expect(Defaults.getPort(normalisedOptions)).to.equal(443); + Defaults.ENVIRONMENT = ''; + }); + + it('closeOnUnload', function () { + var options; + + /* Default to true */ + options = Defaults.normaliseOptions({}); + expect(options.closeOnUnload).to.equal(true); + + /* Default to false if using manual recovery */ + options = Defaults.normaliseOptions({ recover: 'someRecoveryKey' }); + expect(options.closeOnUnload).to.equal(false); + + /* Default to false if using autorecovery */ + options = Defaults.normaliseOptions({ recover: function () {} }); + expect(options.closeOnUnload).to.equal(false); + + /* can override default with manual recovery */ + options = Defaults.normaliseOptions({ recover: 'someRecoveryKey', closeOnUnload: true }); + expect(options.closeOnUnload).to.equal(true); + + /* can override default with autorecovery only at the cost of unsetting autorecovery */ + options = Defaults.normaliseOptions({ recover: function () {}, closeOnUnload: true }); + expect(options.closeOnUnload).to.equal(true); + expect(!options.recover).to.be.ok; + }); + }); +}); diff --git a/spec/rest/fallbacks.test.js b/spec/rest/fallbacks.test.js index d0a89b4b6f..134c49eb08 100644 --- a/spec/rest/fallbacks.test.js +++ b/spec/rest/fallbacks.test.js @@ -1,78 +1,89 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}, - utils = helper.Utils, - goodHost; +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var expect = chai.expect; + var utils = helper.Utils; + var goodHost; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - - goodHost = helper.AblyRest().options.restHost; - test.ok(true, 'app set up'); - test.done(); + describe('rest/fallbacks', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + goodHost = helper.AblyRest().options.restHost; + done(); + }); }); - }; - /* RSC15f */ - exports.store_working_fallback = function(test) { - test.expect(9); - var rest = helper.AblyRest({ - restHost: helper.unroutableHost, - fallbackHosts: [goodHost], - httpRequestTimeout: 3000, - log: {level: 4} + /* RSC15f */ + it('Store working fallback', function (done) { + var rest = helper.AblyRest({ + restHost: helper.unroutableHost, + fallbackHosts: [goodHost], + httpRequestTimeout: 3000, + log: { level: 4 } + }); + var validUntil; + async.series( + [ + function (cb) { + rest.time(function (err, serverTime) { + if (err) { + return cb(err); + } + expect(serverTime, 'Check serverTime returned').to.be.ok; + var currentFallback = rest._currentFallback; + expect(currentFallback, 'Check current fallback stored').to.be.ok; + expect(currentFallback && currentFallback.host).to.equal(goodHost, 'Check good host set'); + validUntil = currentFallback.validUntil; + cb(); + }); + }, + /* now try again, check that this time it uses the remembered good endpoint straight away */ + function (cb) { + rest.time(function (err, serverTime) { + if (err) { + return cb(err); + } + expect(serverTime, 'Check serverTime returned').to.be.ok; + var currentFallback = rest._currentFallback; + expect( + currentFallback.validUntil).to.equal( + validUntil, + 'Check validUntil is the same (implying currentFallback has not been re-set)' + ); + cb(); + }); + }, + /* set the validUntil to the past and check that the stored fallback is forgotten */ + function (cb) { + var now = utils.now(); + rest._currentFallback.validUntil = now - 1000; + rest.time(function (err, serverTime) { + if (err) { + return cb(err); + } + expect(serverTime, 'Check serverTime returned').to.be.ok; + var currentFallback = rest._currentFallback; + expect(currentFallback, 'Check current fallback re-stored').to.be.ok; + expect(currentFallback && currentFallback.host).to.equal(goodHost, 'Check good host set again'); + expect(currentFallback.validUntil > now, 'Check validUntil has been re-set').to.be.ok; + cb(); + }); + } + ], + function (err) { + if (err) { + done(err); + return; + } + done(); + } + ); }); - var validUntil; - async.series([ - function(cb) { - rest.time(function(err, serverTime) { - if(err) { return cb(err); } - test.ok(serverTime, 'Check serverTime returned'); - var currentFallback = rest._currentFallback; - test.ok(currentFallback, 'Check current fallback stored'); - test.equal(currentFallback && currentFallback.host, goodHost, 'Check good host set'); - validUntil = currentFallback.validUntil; - cb(); - }); - }, - /* now try again, check that this time it uses the remembered good endpoint straight away */ - function(cb) { - rest.time(function(err, serverTime) { - if(err) { return cb(err); } - test.ok(serverTime, 'Check serverTime returned'); - var currentFallback = rest._currentFallback; - test.equal(currentFallback.validUntil, validUntil, 'Check validUntil is the same (implying currentFallback has not been re-set)'); - cb(); - }); - }, - /* set the validUntil to the past and check that the stored fallback is forgotten */ - function(cb) { - var now = utils.now(); - rest._currentFallback.validUntil = now - 1000; - rest.time(function(err, serverTime) { - if(err) { return cb(err); } - test.ok(serverTime, 'Check serverTime returned'); - var currentFallback = rest._currentFallback; - test.ok(currentFallback, 'Check current fallback re-stored'); - test.equal(currentFallback && currentFallback.host, goodHost, 'Check good host set again'); - test.ok(currentFallback.validUntil > now, 'Check validUntil has been re-set'); - cb(); - }); - }, - ], function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } - test.done(); - }) - }; - - helper.withMocha('rest/fallbacks', exports); + }); }); diff --git a/spec/rest/history.test.js b/spec/rest/history.test.js index 2d8b3b38d0..92cc1fcde8 100644 --- a/spec/rest/history.test.js +++ b/spec/rest/history.test.js @@ -1,452 +1,483 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var currentTime, rest, restBinary, exports = {}, - restTestOnJsonMsgpack = helper.restTestOnJsonMsgpack, - displayError = helper.displayError, - utils = helper.Utils, - testMessages = [ - { name: 'event0', - data: 'some data' }, - { name: 'event1', - data: 'some more data' }, - { name: 'event2', - data: 'and more' }, - { name: 'event3', - data: 'and more' }, - { name: 'event4', - data: [1,2,3] }, - { name: 'event5', - data: {one: 1, two: 2, three: 3} }, - { name: 'event6', - data: {foo: 'bar'} } - ]; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function() { - rest = helper.AblyRest(); - test.ok(true, 'Setup REST library'); - test.done(); +'use strict'; + +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var rest; + var expect = chai.expect; + var exports = {}; + var restTestOnJsonMsgpack = helper.restTestOnJsonMsgpack; + var utils = helper.Utils; + var testMessages = [ + { name: 'event0', data: 'some data' }, + { name: 'event1', data: 'some more data' }, + { name: 'event2', data: 'and more' }, + { name: 'event3', data: 'and more' }, + { name: 'event4', data: [1, 2, 3] }, + { name: 'event5', data: { one: 1, two: 2, three: 3 } }, + { name: 'event6', data: { foo: 'bar' } } + ]; + + describe('rest/history', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function () { + rest = helper.AblyRest(); + done(); + }); }); - }; - restTestOnJsonMsgpack(exports, 'history_simple', function(test, rest, channelName) { - test.expect(2); - var testchannel = rest.channels.get('persisted:' + channelName); + restTestOnJsonMsgpack('history_simple', function (done, rest, channelName) { + var testchannel = rest.channels.get('persisted:' + channelName); - /* first, send a number of events to this channel */ + /* first, send a number of events to this channel */ - var publishTasks = utils.arrMap(testMessages, function(event) { - return function(publishCb) { - testchannel.publish(event.name, event.data, publishCb); - }; - }); - - publishTasks.push(function(waitCb) { setTimeout(function() { - waitCb(null); - }, 1000); }); - try { - async.parallel(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } + var publishTasks = utils.arrMap(testMessages, function (event) { + return function (publishCb) { + testchannel.publish(event.name, event.data, publishCb); + }; + }); - /* so now the messages are there; try querying the timeline */ - testchannel.history(function(err, resultPage) { - //console.log(require('util').inspect(messages)); - if(err) { - test.ok(false, displayError(err)); - test.done(); + publishTasks.push(function (waitCb) { + setTimeout(function () { + waitCb(null); + }, 1000); + }); + try { + async.parallel(publishTasks, function (err) { + if (err) { + done(err); return; } - /* verify all messages are received */ - var messages = resultPage.items; - test.equal(messages.length, testMessages.length, 'Verify correct number of messages found'); - - /* verify message ids are unique */ - var ids = {}; - utils.arrForEach(messages, function(msg) { ids[msg.id] = msg; }); - test.equal(utils.keysArray(ids).length, testMessages.length, 'Verify correct number of distinct message ids found'); - test.done(); + + /* so now the messages are there; try querying the timeline */ + testchannel.history(function (err, resultPage) { + if (err) { + done(err); + return; + } + /* verify all messages are received */ + var messages = resultPage.items; + expect(messages.length).to.equal(testMessages.length, 'Verify correct number of messages found'); + + /* verify message ids are unique */ + var ids = {}; + utils.arrForEach(messages, function (msg) { + ids[msg.id] = msg; + }); + expect(utils.keysArray(ids).length).to.equal( + testMessages.length, + 'Verify correct number of distinct message ids found' + ); + done(); + }); }); - }); - } catch(e) { - console.log(e.stack); - } - }); + } catch (err) { + done(err); + } + }); + + restTestOnJsonMsgpack('history_multiple', function (done, rest, channelName) { + var testchannel = rest.channels.get('persisted:' + channelName); - restTestOnJsonMsgpack(exports, 'history_multiple', function(test, rest, channelName) { - test.expect(2); - var testchannel = rest.channels.get('persisted:' + channelName); - - /* first, send a number of events to this channel */ - var publishTasks = [function(publishCb) { - testchannel.publish(testMessages, publishCb); - }]; - - publishTasks.push(function(waitCb) { setTimeout(function() { - waitCb(null); - }, 1000); }); - try { - async.parallel(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; + /* first, send a number of events to this channel */ + var publishTasks = [ + function (publishCb) { + testchannel.publish(testMessages, publishCb); } + ]; - /* so now the messages are there; try querying the timeline */ - testchannel.history(function(err, resultPage) { - //console.log(require('util').inspect(messages)); - if(err) { - test.ok(false, displayError(err)); - test.done(); + publishTasks.push(function (waitCb) { + setTimeout(function () { + waitCb(null); + }, 1000); + }); + try { + async.parallel(publishTasks, function (err) { + if (err) { + done(err); return; } - /* verify all messages are received */ - var messages = resultPage.items; - test.equal(messages.length, testMessages.length, 'Verify correct number of messages found'); - - /* verify message ids are unique */ - var ids = {}; - utils.arrForEach(messages, function(msg) { ids[msg.id] = msg; }); - test.equal(utils.keysArray(ids).length, testMessages.length, 'Verify correct number of distinct message ids found'); - test.done(); - }); - }); - } catch(e) { - console.log(e.stack); - } - }); - - restTestOnJsonMsgpack(exports, 'history_simple_paginated_b', function(test, rest, channelName) { - var testchannel = rest.channels.get('persisted:' + channelName); - - /* first, send a number of events to this channel */ - test.expect(5 * testMessages.length - 1); - var publishTasks = utils.arrMap(testMessages, function(event) { - return function(publishCb) { - testchannel.publish(event.name, event.data, publishCb); - }; - }); - publishTasks.push(function(waitCb) { setTimeout(function() { - waitCb(null); - }, 1000); }); - try { - async.series(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - - /* so now the messages are there; try querying the timeline to get messages one at a time */ - var ids = {}, - totalMessagesExpected = testMessages.length, - nextPage = function(cb) { - testchannel.history({limit: 1, direction: 'backwards'}, cb); - }; - - testMessages.reverse(); - async.mapSeries(testMessages, function(expectedMessage, cb) { - nextPage(function(err, resultPage) { - if(err) { - cb(err); + /* so now the messages are there; try querying the timeline */ + testchannel.history(function (err, resultPage) { + if (err) { + done(err); return; } - /* verify expected number of messages in this page */ - test.equal(resultPage.items.length, 1, 'Verify a single message received'); - var resultMessage = resultPage.items[0]; - ids[resultMessage.id] = resultMessage; - - /* verify expected message */ - test.equal(expectedMessage.name, resultMessage.name, 'Verify expected name value present'); - test.deepEqual(expectedMessage.data, resultMessage.data, 'Verify expected data value present'); - - if(--totalMessagesExpected > 0) { - test.ok(resultPage.hasNext(), 'Verify next link is present'); - test.ok(!resultPage.isLast(), 'Verify not last page'); - nextPage = resultPage.next; - } - cb(); + /* verify all messages are received */ + var messages = resultPage.items; + expect(messages.length).to.equal(testMessages.length, 'Verify correct number of messages found'); + + /* verify message ids are unique */ + var ids = {}; + utils.arrForEach(messages, function (msg) { + ids[msg.id] = msg; + }); + expect(utils.keysArray(ids).length).to.equal( + testMessages.length, + 'Verify correct number of distinct message ids found' + ); + done(); }); - }, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + }); + } catch (err) { + done(err); + } + }); + + restTestOnJsonMsgpack('history_simple_paginated_b', function (done, rest, channelName) { + var testchannel = rest.channels.get('persisted:' + channelName); + + /* first, send a number of events to this channel */ + var publishTasks = utils.arrMap(testMessages, function (event) { + return function (publishCb) { + testchannel.publish(event.name, event.data, publishCb); + }; + }); + + publishTasks.push(function (waitCb) { + setTimeout(function () { + waitCb(null); + }, 1000); + }); + try { + async.series(publishTasks, function (err) { + if (err) { + done(err); return; } - /* verify message ids are unique */ - test.equal(utils.keysArray(ids).length, testMessages.length, 'Verify correct number of distinct message ids found'); - test.done(); + + /* so now the messages are there; try querying the timeline to get messages one at a time */ + var ids = {}, + totalMessagesExpected = testMessages.length, + nextPage = function (cb) { + testchannel.history({ limit: 1, direction: 'backwards' }, cb); + }; + + testMessages.reverse(); + async.mapSeries( + testMessages, + function (expectedMessage, cb) { + nextPage(function (err, resultPage) { + if (err) { + cb(err); + return; + } + /* verify expected number of messages in this page */ + expect(resultPage.items.length).to.equal(1, 'Verify a single message received'); + var resultMessage = resultPage.items[0]; + ids[resultMessage.id] = resultMessage; + + /* verify expected message */ + expect(expectedMessage.name).to.equal(resultMessage.name, 'Verify expected name value present'); + expect(expectedMessage.data).to.deep.equal(resultMessage.data, 'Verify expected data value present'); + + if (--totalMessagesExpected > 0) { + expect(resultPage.hasNext(), 'Verify next link is present').to.be.ok; + expect(!resultPage.isLast(), 'Verify not last page').to.be.ok; + nextPage = resultPage.next; + } + cb(); + }); + }, + function (err) { + if (err) { + done(err); + return; + } + /* verify message ids are unique */ + expect(utils.keysArray(ids).length).to.equal( + testMessages.length, + 'Verify correct number of distinct message ids found' + ); + done(); + } + ); }); + } catch (err) { + done(err); + } + }); + + it('history_simple_paginated_f', function (done) { + var testchannel = rest.channels.get('persisted:history_simple_paginated_f'); + + /* first, send a number of events to this channel */ + var publishTasks = utils.arrMap(testMessages, function (event) { + return function (publishCb) { + testchannel.publish(event.name, event.data, publishCb); + }; }); - } catch(e) { - console.log(e.stack); - } - }); - exports.history_simple_paginated_f = function(test) { - var testchannel = rest.channels.get('persisted:history_simple_paginated_f'); + publishTasks.push(function (waitCb) { + setTimeout(function () { + waitCb(null); + }, 1000); + }); + try { + async.series(publishTasks, function (err) { + if (err) { + done(err); + return; + } - /* first, send a number of events to this channel */ - test.expect(4 * testMessages.length); - var publishTasks = utils.arrMap(testMessages, function(event) { - return function(publishCb) { - testchannel.publish(event.name, event.data, publishCb); - }; + /* so now the messages are there; try querying the timeline to get messages one at a time */ + var ids = {}, + totalMessagesExpected = testMessages.length, + nextPage = function (cb) { + testchannel.history({ limit: 1, direction: 'forwards' }, cb); + }; + + async.mapSeries( + testMessages, + function (expectedMessage, cb) { + nextPage(function (err, resultPage) { + if (err) { + cb(err); + return; + } + /* verify expected number of messages in this page */ + expect(resultPage.items.length).to.equal(1, 'Verify a single message received'); + var resultMessage = resultPage.items[0]; + ids[resultMessage.id] = resultMessage; + + /* verify expected message */ + expect(expectedMessage.name).to.equal(resultMessage.name, 'Verify expected name value present'); + expect(expectedMessage.data).to.deep.equal(resultMessage.data, 'Verify expected data value present'); + + if (--totalMessagesExpected > 0) { + expect(resultPage.hasNext(), 'Verify next link is present').to.be.ok; + nextPage = resultPage.next; + } + cb(); + }); + }, + function (err) { + if (err) { + done(err); + return; + } + /* verify message ids are unique */ + expect(utils.keysArray(ids).length).to.equal( + testMessages.length, + 'Verify correct number of distinct message ids found' + ); + done(); + } + ); + }); + } catch (err) { + done(err); + } }); - publishTasks.push(function(waitCb) { setTimeout(function() { - waitCb(null); - }, 1000); }); - try { - async.series(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; + it('history_multiple_paginated_b', function (done) { + var testchannel = rest.channels.get('persisted:history_multiple_paginated_b'); + + /* first, send a number of events to this channel */ + var publishTasks = [ + function (publishCb) { + testchannel.publish(testMessages, publishCb); } + ]; - /* so now the messages are there; try querying the timeline to get messages one at a time */ - var ids = {}, - totalMessagesExpected = testMessages.length, - nextPage = function(cb) { - testchannel.history({limit: 1, direction: 'forwards'}, cb); - }; - - async.mapSeries(testMessages, function(expectedMessage, cb) { - nextPage(function(err, resultPage) { - if(err) { - cb(err); - return; - } - /* verify expected number of messages in this page */ - test.equal(resultPage.items.length, 1, 'Verify a single message received'); - var resultMessage = resultPage.items[0]; - ids[resultMessage.id] = resultMessage; - - /* verify expected message */ - test.equal(expectedMessage.name, resultMessage.name, 'Verify expected name value present'); - test.deepEqual(expectedMessage.data, resultMessage.data, 'Verify expected data value present'); - - if(--totalMessagesExpected > 0) { - test.ok(resultPage.hasNext(), 'Verify next link is present'); - nextPage = resultPage.next; - } - cb(); - }); - }, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + publishTasks.push(function (waitCb) { + setTimeout(function () { + waitCb(null); + }, 1000); + }); + try { + async.series(publishTasks, function (err) { + if (err) { + done(err); return; } - /* verify message ids are unique */ - test.equal(utils.keysArray(ids).length, testMessages.length, 'Verify correct number of distinct message ids found'); - test.done(); + + /* so now the messages are there; try querying the timeline to get messages one at a time */ + var ids = {}, + totalMessagesExpected = testMessages.length, + nextPage = function (cb) { + testchannel.history({ limit: 1, direction: 'backwards' }, cb); + }; + + testMessages.reverse(); + async.mapSeries( + testMessages, + function (expectedMessage, cb) { + nextPage(function (err, resultPage) { + if (err) { + cb(err); + return; + } + /* verify expected number of messages in this page */ + expect(resultPage.items.length).to.equal(1, 'Verify a single message received'); + var resultMessage = resultPage.items[0]; + ids[resultMessage.id] = resultMessage; + + /* verify expected message */ + expect(expectedMessage.name).to.equal(resultMessage.name, 'Verify expected name value present'); + expect(expectedMessage.data).to.deep.equal(resultMessage.data, 'Verify expected data value present'); + + if (--totalMessagesExpected > 0) { + expect(resultPage.hasNext(), 'Verify next link is present').to.be.ok; + nextPage = resultPage.next; + } + cb(); + }); + }, + function (err) { + if (err) { + done(err); + return; + } + /* verify message ids are unique */ + expect(utils.keysArray(ids).length).to.equal( + testMessages.length, + 'Verify correct number of distinct message ids found' + ); + done(); + } + ); }); - }); - } catch(e) { - console.log(e.stack); - } - }; - - exports.history_multiple_paginated_b = function(test) { - var testchannel = rest.channels.get('persisted:history_multiple_paginated_b'); - - /* first, send a number of events to this channel */ - test.expect(4 * testMessages.length); - var publishTasks = [function(publishCb) { - testchannel.publish(testMessages, publishCb); - }]; - - publishTasks.push(function(waitCb) { setTimeout(function() { - waitCb(null); - }, 1000); }); - try { - async.series(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; + } catch (err) { + done(err); + } + }); + + it('history_multiple_paginated_f', function (done) { + var testchannel = rest.channels.get('persisted:history_multiple_paginated_f'); + + /* first, send a number of events to this channel */ + var publishTasks = [ + function (publishCb) { + testchannel.publish(testMessages, publishCb); } + ]; - /* so now the messages are there; try querying the timeline to get messages one at a time */ - var ids = {}, - totalMessagesExpected = testMessages.length, - nextPage = function(cb) { - testchannel.history({limit: 1, direction: 'backwards'}, cb); - }; - - testMessages.reverse(); - async.mapSeries(testMessages, function(expectedMessage, cb) { - nextPage(function(err, resultPage) { - if(err) { - cb(err); - return; - } - /* verify expected number of messages in this page */ - test.equal(resultPage.items.length, 1, 'Verify a single message received'); - var resultMessage = resultPage.items[0]; - ids[resultMessage.id] = resultMessage; - - /* verify expected message */ - test.equal(expectedMessage.name, resultMessage.name, 'Verify expected name value present'); - test.deepEqual(expectedMessage.data, resultMessage.data, 'Verify expected data value present'); - - if(--totalMessagesExpected > 0) { - test.ok(resultPage.hasNext(), 'Verify next link is present'); - nextPage = resultPage.next; - } - cb(); - }); - }, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + publishTasks.push(function (waitCb) { + setTimeout(function () { + waitCb(null); + }, 1000); + }); + try { + async.series(publishTasks, function (err) { + if (err) { + done(err); return; } - /* verify message ids are unique */ - test.equal(utils.keysArray(ids).length, testMessages.length, 'Verify correct number of distinct message ids found'); - test.done(); - }); - }); - } catch(e) { - console.log(e.stack); - } - }; - - exports.history_multiple_paginated_f = function(test) { - var testchannel = rest.channels.get('persisted:history_multiple_paginated_f'); - - /* first, send a number of events to this channel */ - test.expect(4 * testMessages.length); - var publishTasks = [function(publishCb) { - testchannel.publish(testMessages, publishCb); - }]; - - publishTasks.push(function(waitCb) { setTimeout(function() { - waitCb(null); - }, 1000); }); - try { - async.series(publishTasks, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - /* so now the messages are there; try querying the timeline to get messages one at a time */ - var ids = {}, - totalMessagesExpected = testMessages.length, - nextPage = function(cb) { - testchannel.history({limit: 1, direction: 'forwards'}, cb); - }; - - async.mapSeries(testMessages, function(expectedMessage, cb) { - nextPage(function(err, resultPage) { - if(err) { - cb(err); - return; - } - /* verify expected number of messages in this page */ - test.equal(resultPage.items.length, 1, 'Verify a single message received'); - var resultMessage = resultPage.items[0]; - ids[resultMessage.id] = resultMessage; - - /* verify expected message */ - test.equal(expectedMessage.name, resultMessage.name, 'Verify expected name value present'); - test.deepEqual(expectedMessage.data, resultMessage.data, 'Verify expected data value present'); - - if(--totalMessagesExpected > 0) { - test.ok(resultPage.hasNext(), 'Verify next link is present'); - nextPage = resultPage.next; + /* so now the messages are there; try querying the timeline to get messages one at a time */ + var ids = {}, + totalMessagesExpected = testMessages.length, + nextPage = function (cb) { + testchannel.history({ limit: 1, direction: 'forwards' }, cb); + }; + + async.mapSeries( + testMessages, + function (expectedMessage, cb) { + nextPage(function (err, resultPage) { + if (err) { + cb(err); + return; + } + /* verify expected number of messages in this page */ + expect(resultPage.items.length).to.equal(1, 'Verify a single message received'); + var resultMessage = resultPage.items[0]; + ids[resultMessage.id] = resultMessage; + + /* verify expected message */ + expect(expectedMessage.name).to.equal(resultMessage.name, 'Verify expected name value present'); + expect(expectedMessage.data).to.deep.equal(resultMessage.data, 'Verify expected data value present'); + + if (--totalMessagesExpected > 0) { + expect(resultPage.hasNext(), 'Verify next link is present').to.be.ok; + nextPage = resultPage.next; + } + cb(); + }); + }, + function (err) { + if (err) { + done(err); + return; + } + /* verify message ids are unique */ + expect(utils.keysArray(ids).length).to.equal( + testMessages.length, + 'Verify correct number of distinct message ids found' + ); + done(); } - cb(); - }); - }, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + ); + }); + } catch (err) { + done(err); + } + }); + + restTestOnJsonMsgpack('history_encoding_errors', function (done, rest, channelName) { + var testchannel = rest.channels.get('persisted:' + channelName); + var badMessage = { name: 'jsonUtf8string', encoding: 'json/utf-8', data: '{"foo":"bar"}' }; + try { + testchannel.publish(badMessage, function (err) { + if (err) { + done(err); return; } - /* verify message ids are unique */ - test.equal(utils.keysArray(ids).length, testMessages.length, 'Verify correct number of distinct message ids found'); - test.done(); + setTimeout(function () { + testchannel.history(function (err, resultPage) { + if (err) { + done(err); + return; + } + /* verify all messages are received */ + var message = resultPage.items[0]; + expect(message.data).to.equal(badMessage.data, 'Verify data preserved'); + expect(message.encoding).to.equal(badMessage.encoding, 'Verify encoding preserved'); + done(); + }); + }, 1000); }); + } catch (err) { + done(err); + } + }); + + if (typeof Promise !== 'undefined') { + it('historyPromise', function (done) { + var rest = helper.AblyRest({ promises: true }); + var testchannel = rest.channels.get('persisted:history_promise'); + + testchannel + .publish('one', null) + .then(function () { + return testchannel.publish('two', null); + }) + .then(function () { + return testchannel.history({ limit: 1, direction: 'forwards' }); + }) + .then(function (resultPage) { + expect(resultPage.items.length).to.equal(1); + expect(resultPage.items[0].name).to.equal('one'); + return resultPage.first(); + }) + .then(function (resultPage) { + expect(resultPage.items[0].name).to.equal('one'); + return resultPage.current(); + }) + .then(function (resultPage) { + expect(resultPage.items[0].name).to.equal('one'); + return resultPage.next(); + }) + .then(function (resultPage) { + expect(resultPage.items[0].name).to.equal('two'); + done(); + }) + ['catch'](function (err) { + done(err); + }) }); - } catch(e) { - console.log(e.stack); - } - }; - - restTestOnJsonMsgpack(exports, 'history_encoding_errors', function(test, rest, channelName) { - var testchannel = rest.channels.get('persisted:' + channelName); - var badMessage = {name: 'jsonUtf8string', encoding: 'json/utf-8', data: '{\"foo\":\"bar\"}'}; - test.expect(2); - try { - testchannel.publish(badMessage, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - setTimeout(function(){ - testchannel.history(function(err, resultPage) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - /* verify all messages are received */ - var message = resultPage.items[0]; - test.equal(message.data, badMessage.data, 'Verify data preserved'); - test.equal(message.encoding, badMessage.encoding, 'Verify encoding preserved'); - test.done(); - }); - }, 1000); - }); - } catch(e) { - console.log(e.stack); } }); - - exports.historyPromise = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; - } - test.expect(5); - var rest = helper.AblyRest({promises: true}); - var testchannel = rest.channels.get('persisted:history_promise'); - - testchannel.publish('one', null).then(function() { - return testchannel.publish('two', null); - }).then(function() { - return testchannel.history({limit: 1, direction: 'forwards'}); - }).then(function(resultPage) { - test.equal(resultPage.items.length, 1); - test.equal(resultPage.items[0].name, 'one'); - return resultPage.first(); - }).then(function(resultPage) { - test.equal(resultPage.items[0].name, 'one'); - return resultPage.current(); - }).then(function(resultPage) { - test.equal(resultPage.items[0].name, 'one'); - return resultPage.next(); - }).then(function(resultPage) { - test.equal(resultPage.items[0].name, 'two'); - })['catch'](function(err) { - test.ok(false, 'Promise chain failed with error: ' + displayError(err)); - }).finally(function() { - test.done(); - }); - }; - - helper.withMocha('rest/history', exports); -}); \ No newline at end of file +}); diff --git a/spec/rest/http.test.js b/spec/rest/http.test.js index 0f70633352..aa4ef8d4bf 100644 --- a/spec/rest/http.test.js +++ b/spec/rest/http.test.js @@ -1,66 +1,67 @@ 'use strict'; -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var rest, exports = {}, - Defaults = Ably.Rest.Defaults; +define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { + var rest; + var expect = chai.expect; + var Defaults = Ably.Rest.Defaults; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function() { - rest = helper.AblyRest(); - test.ok(true, 'App created'); - test.done(); + describe('rest/http', function () { + this.timeout(60 * 1000); + before(function (done) { + helper.setupApp(function () { + rest = helper.AblyRest(); + done(); + }); }); - } - /** - * Check presence of X-Ably-Version headers in get&post requests - * @spec : (RSC7a) - */ - exports.apiVersionHeader = function(test) { + /** + * RSC7a + */ + it('Should send X-Ably-Version and X-Ably-Lib headers in get/post requests', function (done) { + //Intercept get&post methods with test + var get_inner = Ably.Rest.Http.get; + Ably.Rest.Http.get = function (rest, path, headers, params, callback) { + try { + expect('X-Ably-Version' in headers, 'Verify version header exists').to.be.ok; + expect('X-Ably-Lib' in headers, 'Verify lib header exists').to.be.ok; - //Intercept get&post methods with test - var get_inner = Ably.Rest.Http.get; - Ably.Rest.Http.get = function (rest, path, headers, params, callback) { - test.ok(('X-Ably-Version' in headers), 'Verify version header exists'); - test.ok(('X-Ably-Lib' in headers), 'Verify lib header exists'); - - // This test should not directly validate version against Defaults.version, as - // ultimately the version header has been derived from that value. - test.equal(headers['X-Ably-Version'], '1.2', 'Verify current version number'); + // This test should not directly validate version against Defaults.version, as + // ultimately the version header has been derived from that value. + expect(headers['X-Ably-Version']).to.equal('1.2', 'Verify current version number'); + expect(headers['X-Ably-Lib'].indexOf(Defaults.version) > -1, 'Verify libstring').to.be.ok; + } catch (err) { + done(err); + } + }; - test.ok(headers['X-Ably-Lib'].indexOf(Defaults.version) > -1, 'Verify libstring'); - }; + var post_inner = Ably.Rest.Http.post; + Ably.Rest.Http.post = function (rest, path, headers, body, params, callback) { + try { + expect('X-Ably-Version' in headers, 'Verify version header exists').to.be.ok; + expect('X-Ably-Lib' in headers, 'Verify lib header exists').to.be.ok; - var post_inner = Ably.Rest.Http.post; - Ably.Rest.Http.post = function (rest, path, headers, body, params, callback) { - test.ok(('X-Ably-Version' in headers), 'Verify version header exists'); - test.ok(('X-Ably-Lib' in headers), 'Verify lib header exists'); + // This test should not directly validate version against Defaults.version, as + // ultimately the version header has been derived from that value. + expect(headers['X-Ably-Version']).to.equal('1.2', 'Verify current version number'); + expect(headers['X-Ably-Lib'].indexOf(Defaults.version) > -1, 'Verify libstring').to.be.ok; + } catch (err) { + done(err); + } + }; - // This test should not directly validate version against Defaults.version, as - // ultimately the version header has been derived from that value. - test.equal(headers['X-Ably-Version'], '1.2', 'Verify current version number'); - - test.ok(headers['X-Ably-Lib'].indexOf(Defaults.version) > -1, 'Verify libstring'); - }; + //Call all methods that use rest http calls + rest.auth.requestToken(); + rest.time(); + rest.stats(); + var channel = rest.channels.get('http_test_channel'); + channel.publish('test', 'Testing http headers'); + channel.presence.get(); - //Call all methods that use rest http calls - test.expect(20); + //Clean interceptors from get&post methods + Ably.Rest.Http.get = get_inner; + Ably.Rest.Http.post = post_inner; - rest.auth.requestToken(); - rest.time(); - rest.stats(); - - var channel = rest.channels.get('http_test_channel'); - channel.publish('test', 'Testing http headers'); - channel.presence.get(); - - //Clean interceptors from get&post methods - Ably.Rest.Http.get = get_inner; - Ably.Rest.Http.post = post_inner; - - test.done(); - }; - - helper.withMocha('rest/http', exports); -}); \ No newline at end of file + done(); + }); + }); +}); diff --git a/spec/rest/init.test.js b/spec/rest/init.test.js index 9a6cd5a7c3..b01024e6dd 100644 --- a/spec/rest/init.test.js +++ b/spec/rest/init.test.js @@ -1,121 +1,96 @@ -"use strict"; - -define(['ably', 'shared_helper'], function(Ably, helper) { - var exports = {}; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); - }; - - /* init with key string */ - exports.init_key_string = function(test) { - test.expect(1); - try { - var keyStr = helper.getTestApp().keys[0].keyStr, - rest = new helper.Ably.Rest(keyStr); - - test.equal(rest.options.key, keyStr); - test.done(); - } catch(e) { - test.ok(false, 'Init with key failed with exception: ' + e.stack); - test.done(); - } - }; - - /* init with token string */ - exports.init_token_string = function(test) { - test.expect(1); - try { - /* first generate a token ... */ - var rest = helper.AblyRest(); - var testKeyOpts = {key: helper.getTestApp().keys[1].keyStr}; - - rest.auth.requestToken(null, testKeyOpts, function(err, tokenDetails) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); +'use strict'; + +define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { + var expect = chai.expect; + + describe('rest/init', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); return; } + done(); + }); + }); - var tokenStr = tokenDetails.token, - rest = new helper.Ably.Rest(tokenStr); + it('Init with key string', function () { + var keyStr = helper.getTestApp().keys[0].keyStr; + var rest = new helper.Ably.Rest(keyStr); - test.equal(rest.options.token, tokenStr); - test.done(); - }); - } catch(e) { - test.ok(false, 'Init with token failed with exception: ' + e.stack); - test.done(); - } - }; - - /* init with tls: false */ - exports.init_tls_false = function(test) { - test.expect(1); - var rest = helper.AblyRest({tls: false, port: 123, tlsPort: 456}); - test.equal(rest.baseUri("example.com"), "http://example.com:123") - test.done(); - }; - - /* init with tls: true */ - exports.init_tls_true= function(test) { - test.expect(1); - var rest = helper.AblyRest({tls: true, port: 123, tlsPort: 456}); - test.equal(rest.baseUri("example.com"), "https://example.com:456") - test.done(); - }; - - /* init without any tls key should enable tls */ - exports.init_tls_absent = function(test) { - test.expect(1); - var rest = helper.AblyRest({port: 123, tlsPort: 456}); - test.equal(rest.baseUri("example.com"), "https://example.com:456") - test.done(); - }; - - /* init with a clientId set to '*', or anything other than a string or null, - * should raise an exception */ - exports.init_wildcard_clientId = function(test) { - test.expect(3); - test.throws(function() { - var rest = helper.AblyRest({clientId: '*'}); - }, 'Check can’t init library with a wildcard clientId'); - test.throws(function() { - var rest = helper.AblyRest({clientId: 123}); - }, 'Check can’t init library with a numerical clientId'); - test.throws(function() { - var rest = helper.AblyRest({clientId: false}); - }, 'Check can’t init library with a boolean clientId'); - test.done(); - }; - - exports.init_callbacks_promises = function(test) { - var rest, - keyStr = helper.getTestApp().keys[0].keyStr; - - rest = new Ably.Rest(keyStr); - test.ok(!rest.options.promises, 'Check promises defaults to false'); - - rest = new Ably.Rest.Promise(keyStr); - test.ok(rest.options.promises, 'Check promises default to true with promise constructor'); - - if(!isBrowser && typeof require == 'function') { - rest = new require('../../promises').Rest(keyStr); - test.ok(rest.options.promises, 'Check promises default to true with promise require target'); - - rest = new require('../../callbacks').Rest(keyStr); - test.ok(!rest.options.promises, 'Check promises default to false with callback require target'); - } - test.done(); - }; - - helper.withMocha('rest/init', exports); -}); \ No newline at end of file + expect(rest.options.key).to.equal(keyStr); + }); + + it('Init with token string', function (done) { + try { + /* first generate a token ... */ + var rest = helper.AblyRest(); + var testKeyOpts = { key: helper.getTestApp().keys[1].keyStr }; + + rest.auth.requestToken(null, testKeyOpts, function (err, tokenDetails) { + if (err) { + done(err); + return; + } + + var tokenStr = tokenDetails.token, + rest = new helper.Ably.Rest(tokenStr); + + expect(rest.options.token).to.equal(tokenStr); + done(); + }); + } catch (err) { + done(err); + } + }); + + it('Init with tls: false', function () { + var rest = helper.AblyRest({ tls: false, port: 123, tlsPort: 456 }); + expect(rest.baseUri('example.com')).to.equal('http://example.com:123'); + }); + + it('Init with tls: true', function () { + var rest = helper.AblyRest({ tls: true, port: 123, tlsPort: 456 }); + expect(rest.baseUri('example.com')).to.equal('https://example.com:456'); + }); + + /* init without any tls key should enable tls */ + it('Init without any tls key should enable tls', function () { + var rest = helper.AblyRest({ port: 123, tlsPort: 456 }); + expect(rest.baseUri('example.com')).to.equal('https://example.com:456'); + }); + + it("Init with clientId set to '*' or anything other than a string or null should error", function () { + expect(function () { + var rest = helper.AblyRest({ clientId: '*' }); + }, 'Check can’t init library with a wildcard clientId').to.throw; + expect(function () { + var rest = helper.AblyRest({ clientId: 123 }); + }, 'Check can’t init library with a numerical clientId').to.throw; + expect(function () { + var rest = helper.AblyRest({ clientId: false }); + }, 'Check can’t init library with a boolean clientId').to.throw; + }); + + it('Init promises', function () { + var rest, + keyStr = helper.getTestApp().keys[0].keyStr; + + rest = new Ably.Rest(keyStr); + expect(!rest.options.promises, 'Check promises defaults to false').to.be.ok; + + rest = new Ably.Rest.Promise(keyStr); + expect(rest.options.promises, 'Check promises default to true with promise constructor').to.be.ok; + + if (!isBrowser && typeof require == 'function') { + rest = new require('../../promises').Rest(keyStr); + expect(rest.options.promises, 'Check promises default to true with promise require target').to.be.ok; + + rest = new require('../../callbacks').Rest(keyStr); + expect(!rest.options.promises, 'Check promises default to false with callback require target').to.be.ok; + } + }); + }); +}); diff --git a/spec/rest/message.test.js b/spec/rest/message.test.js index 822aa3586f..e036f598a4 100644 --- a/spec/rest/message.test.js +++ b/spec/rest/message.test.js @@ -1,292 +1,349 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var exports = {}; - var _exports = {}; - var displayError = helper.displayError; - var noop = function() {}; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.ok(true, 'app set up'); - } - test.done(); - }); - }; +'use strict'; +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var expect = chai.expect; + var noop = function () {}; - /* Authenticate with a clientId and ensure that the clientId is not sent in the Message - and is implicitly added when published */ - exports.rest_implicit_client_id_0 = function(test) { - var clientId = 'implicit_client_id_0', - rest = helper.AblyRest({ clientId: clientId, useBinaryProtocol: false }), - channel = rest.channels.get('rest_implicit_client_id_0'); - - test.expect(3); - - var originalPublish = channel._publish; - channel._publish = function(requestBody) { - var message = JSON.parse(requestBody)[0]; - test.ok(message.name === 'event0', 'Outgoing message interecepted'); - test.ok(!message.clientId, 'client ID is not added by the client library as it is implicit'); - originalPublish.apply(channel, arguments); - }; - - channel.publish('event0', null, function(err) { - if (err) { - test.ok(false, 'Publish failed with implicit clientId: ' + displayError(err)); - return test.done(); - } - - channel.history(function(err, page) { + describe('rest/message', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { if (err) { - test.ok(false, 'History failed with implicit clientId: ' + displayError(err)); - return test.done(); + done(err); } - - var message = page.items[0]; - test.ok(message.clientId == clientId, 'Client ID was added implicitly'); - test.done(); + done(); }); }); - }; - /* Authenticate with a clientId and explicitly provide the same clientId in the Message - and ensure it is published */ - exports.rest_explicit_client_id_0 = function(test) { - var clientId = 'explicit_client_id_0', - rest = helper.AblyRest({ clientId: clientId, useBinaryProtocol: false }), - channel = rest.channels.get('rest_explicit_client_id_0'); - - test.expect(3); - - var originalPublish = channel._publish; - channel._publish = function(requestBody) { - var message = JSON.parse(requestBody)[0]; - test.ok(message.name === 'event0', 'Outgoing message interecepted'); - test.ok(message.clientId == clientId, 'client ID is added by the client library as it is explicit in the publish'); - originalPublish.apply(channel, arguments); - }; - - channel.publish({ name: 'event0', clientId: clientId}, function(err) { - if (err) { - test.ok(false, 'Publish failed with explicit clientId: ' + displayError(err)); - return test.done(); - } - - channel.history(function(err, page) { + /* Authenticate with a clientId and ensure that the clientId is not sent in the Message + and is implicitly added when published */ + it('Should implicitly send clientId when authenticated with clientId', function (done) { + var clientId = 'implicit_client_id_0', + rest = helper.AblyRest({ clientId: clientId, useBinaryProtocol: false }), + channel = rest.channels.get('rest_implicit_client_id_0'); + + var originalPublish = channel._publish; + channel._publish = function (requestBody) { + var message = JSON.parse(requestBody)[0]; + try { + expect(message.name === 'event0', 'Outgoing message interecepted').to.be.ok; + expect(!message.clientId, 'client ID is not added by the client library as it is implicit').to.be.ok; + } catch (err) { + done(err); + } + originalPublish.apply(channel, arguments); + }; + + channel.publish('event0', null, function (err) { if (err) { - test.ok(false, 'History failed with explicit clientId: ' + displayError(err)); - return test.done(); + done(err); + return; } - var message = page.items[0]; - test.ok(message.clientId == clientId, 'Client ID was retained'); - test.done(); + channel.history(function (err, page) { + if (err) { + done(err); + return; + } + + var message = page.items[0]; + try { + expect(message.clientId == clientId, 'Client ID was added implicitly').to.be.ok; + done(); + } catch (err) { + done(err); + } + }); }); }); - }; - - /* Authenticate with a clientId and explicitly provide a different invalid clientId in the Message - and expect it to not be published and be rejected */ - exports.rest_explicit_client_id_1 = function(test) { - var clientId = 'explicit_client_id_0', - invalidClientId = 'invalid'; - - test.expect(4); - helper.AblyRest().auth.requestToken({ clientId: clientId }, function(err, token) { - test.ok(token.clientId === clientId, 'client ID is present in the Token'); - - // REST client uses a token string so is unaware of the clientId so cannot reject before communicating with Ably - var rest = helper.AblyRest({ token: token.token, useBinaryProtocol: false }), - channel = rest.channels.get('rest_explicit_client_id_1'); + /* Authenticate with a clientId and explicitly provide the same clientId in the Message + and ensure it is published */ + it('Should publish clientId when provided explicitly in message', function (done) { + var clientId = 'explicit_client_id_0', + rest = helper.AblyRest({ clientId: clientId, useBinaryProtocol: false }), + channel = rest.channels.get('rest_explicit_client_id_0'); var originalPublish = channel._publish; - channel._publish = function(requestBody) { + channel._publish = function (requestBody) { var message = JSON.parse(requestBody)[0]; - test.ok(message.name === 'event0', 'Outgoing message interecepted'); - test.ok(message.clientId == invalidClientId, 'invalid client ID is added by the client library as it is explicit in the publish'); + try { + expect(message.name === 'event0', 'Outgoing message interecepted').to.be.ok; + expect( + message.clientId == clientId, + 'client ID is added by the client library as it is explicit in the publish' + ).to.be.ok; + } catch (err) { + done(err); + } originalPublish.apply(channel, arguments); }; - channel.publish({ name: 'event0', clientId: invalidClientId}, function(err) { - if (!err) { - test.ok(false, 'Publish should have failed with invalid clientId'); - return test.done(); + channel.publish({ name: 'event0', clientId: clientId }, function (err) { + if (err) { + done(err); } - channel.history(function(err, page) { + channel.history(function (err, page) { if (err) { - test.ok(false, 'History failed with explicit clientId: ' + displayError(err)); - return test.done(); + done(err); + return; } - test.equal(page.items.length, 0, 'Message should not have been published'); - test.done(); + var message = page.items[0]; + try { + expect(message.clientId == clientId, 'Client ID was retained').to.be.ok; + done(); + } catch (err) { + done(err); + } }); }); }); - }; - - /* TO3l8; CD2C; RSL1i */ - exports.maxMessageSize = function(test) { - test.expect(2); - /* No connectionDetails mechanism for REST, so just pass the override into the constructor */ - var realtime = helper.AblyRest({maxMessageSize: 64}), - channel = realtime.channels.get('maxMessageSize'); - - channel.publish('foo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', function(err) { - test.ok(err, 'Check publish refused'); - test.equal(err.code, 40009); - test.done(); - }); - }; - - /* Check ids are correctly sent */ - exports.idempotent_rest_publishing = function(test) { - test.expect(2); - var rest = helper.AblyRest({idempotentRestPublishing: false, useBinaryProtocol: false}), - channel = rest.channels.get('idempotent_rest_publishing'), - originalPost = Ably.Rest.Http.post, - originalPublish = channel._publish, - message = {name: 'test', id: 'idempotent-msg-id:0'}; - - async.parallel([ - function(parCb) { channel.publish(message, parCb); }, - function(parCb) { channel.publish(message, parCb); }, - function(parCb) { channel.publish(message, parCb); } - ], function(err) { - if(err) { - test.ok(false, 'Publish failed with error ' + displayError(err)); - return test.done(); - } - - channel.history(function(err, page) { - if(err) { - test.ok(false, 'History failed with error ' + displayError(err)); - return test.done(); + + /* Authenticate with a clientId and explicitly provide a different invalid clientId in the Message + and expect it to not be published and be rejected */ + it('Should error when clientId sent in message is different than authenticated clientId', function (done) { + var clientId = 'explicit_client_id_0', + invalidClientId = 'invalid'; + + helper.AblyRest().auth.requestToken({ clientId: clientId }, function (err, token) { + try { + expect(token.clientId === clientId, 'client ID is present in the Token').to.be.ok; + } catch (err) { + done(err); } - test.equal(page.items.length, 1, 'Check only one message published'); - test.equal(page.items[0].id, message.id, 'Check message id preserved in history'); - test.done(); + + // REST client uses a token string so is unaware of the clientId so cannot reject before communicating with Ably + var rest = helper.AblyRest({ token: token.token, useBinaryProtocol: false }), + channel = rest.channels.get('rest_explicit_client_id_1'); + + var originalPublish = channel._publish; + channel._publish = function (requestBody) { + var message = JSON.parse(requestBody)[0]; + try { + expect(message.name === 'event0', 'Outgoing message interecepted').to.be.ok; + expect( + message.clientId == invalidClientId, + 'invalid client ID is added by the client library as it is explicit in the publish' + ).to.be.ok; + } catch (err) { + done(err); + } + originalPublish.apply(channel, arguments); + }; + + channel.publish({ name: 'event0', clientId: invalidClientId }, function (err) { + if (!err) { + done(new Error('Publish should have failed with invalid clientId')); + return; + } + + channel.history(function (err, page) { + if (err) { + done(err); + return; + } + + try { + expect(page.items.length).to.equal(0, 'Message should not have been published'); + done(); + } catch (err) { + done(err); + } + }); + }); }); }); - }; - - /* Check ids are added when automatic idempotent rest publishing option enabled */ - exports.automatic_idempotent_rest_publishing = function(test) { - /* easiest way to get the host we're using for tests */ - var dummyRest = helper.AblyRest(), - host = dummyRest.options.restHost, - /* Add the same host as a bunch of fallback hosts, so after the first - * request 'fails' we retry on the same host using the fallback mechanism */ - rest = helper.AblyRest({idempotentRestPublishing: true, useBinaryProtocol: false, fallbackHosts: [ host, host, host ]}), - channel = rest.channels.get('automatic_idempotent_rest_publishing'), - originalPost = Ably.Rest.Http.post, - idOne, - idTwo, - originalPublish = channel._publish, - originalDoUri = Ably.Rest.Http.doUri; - - channel._publish = function(requestBody) { - var messageOne = JSON.parse(requestBody)[0]; - var messageTwo = JSON.parse(requestBody)[1]; - test.equal(messageOne.name, 'one', 'Outgoing message 1 interecepted'); - test.equal(messageTwo.name, 'two', 'Outgoing message 2 interecepted'); - idOne = messageOne.id; - idTwo = messageTwo.id; - test.ok(idOne, 'id set on message 1'); - test.ok(idTwo, 'id set on message 2'); - test.equal(idOne && idOne.split(':')[1], '0', 'check zero-based index'); - test.equal(idTwo && idTwo.split(':')[1], '1', 'check zero-based index'); - originalPublish.apply(channel, arguments); - }; - - Ably.Rest.Http.doUri = function(method, rest, uri, headers, body, params, callback) { - originalDoUri(method, rest, uri, headers, body, params, function(err) { - if(err) { - test.ok(false, 'Actual error from first post: ' + displayError(err)); - callback(err); - return; + + /* TO3l8; CD2C; RSL1i */ + it('Should error when publishing message larger than maxMessageSize', function (done) { + /* No connectionDetails mechanism for REST, so just pass the override into the constructor */ + var realtime = helper.AblyRest({ maxMessageSize: 64 }), + channel = realtime.channels.get('maxMessageSize'); + + channel.publish('foo', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', function (err) { + try { + expect(err, 'Check publish refused').to.be.ok; + expect(err.code).to.equal(40009); + done(); + } catch (err) { + done(err); } - /* Fake a publish error from realtime */ - callback({message: 'moo', code: 50300, statusCode: 503}); }); - Ably.Rest.Http.doUri = originalDoUri; - }; - - channel.publish([ {name: 'one'}, {name: 'two'} ], function(err) { - if(err) { - test.ok(false, 'Publish failed with error ' + displayError(err)); - return test.done(); - } - - channel.history({direction: 'forwards'}, function(err, page) { - if(err) { - test.ok(false, 'History failed with error ' + displayError(err)); - return test.done(); + }); + + /* Check ids are correctly sent */ + it('Should send correct IDs when idempotentRestPublishing set to false', function (done) { + var rest = helper.AblyRest({ idempotentRestPublishing: false, useBinaryProtocol: false }), + channel = rest.channels.get('idempotent_rest_publishing'), + message = { name: 'test', id: 'idempotent-msg-id:0' }; + + async.parallel( + [ + function (parCb) { + channel.publish(message, parCb); + }, + function (parCb) { + channel.publish(message, parCb); + }, + function (parCb) { + channel.publish(message, parCb); + } + ], + function (err) { + if (err) { + done(err); + return; + } + + channel.history(function (err, page) { + if (err) { + done(err); + return; + } + try { + expect(page.items.length).to.equal(1, 'Check only one message published'); + expect(page.items[0].id).to.equal(message.id, 'Check message id preserved in history'); + done(); + } catch (err) { + done(err); + } + }); + } + ); + }); + + /* Check ids are added when automatic idempotent rest publishing option enabled */ + it('Should add IDs when automatic idempotent rest publishing option enabled', function (done) { + /* easiest way to get the host we're using for tests */ + var dummyRest = helper.AblyRest(), + host = dummyRest.options.restHost, + /* Add the same host as a bunch of fallback hosts, so after the first + * request 'fails' we retry on the same host using the fallback mechanism */ + rest = helper.AblyRest({ + idempotentRestPublishing: true, + useBinaryProtocol: false, + fallbackHosts: [host, host, host] + }), + channel = rest.channels.get('automatic_idempotent_rest_publishing'), + originalPost = Ably.Rest.Http.post, + idOne, + idTwo, + originalPublish = channel._publish, + originalDoUri = Ably.Rest.Http.doUri; + + channel._publish = function (requestBody) { + try { + var messageOne = JSON.parse(requestBody)[0]; + var messageTwo = JSON.parse(requestBody)[1]; + expect(messageOne.name).to.equal('one', 'Outgoing message 1 interecepted'); + expect(messageTwo.name).to.equal('two', 'Outgoing message 2 interecepted'); + idOne = messageOne.id; + idTwo = messageTwo.id; + expect(idOne, 'id set on message 1').to.be.ok; + expect(idTwo, 'id set on message 2').to.be.ok; + expect(idOne && idOne.split(':')[1]).to.equal('0', 'check zero-based index'); + expect(idTwo && idTwo.split(':')[1]).to.equal('1', 'check zero-based index'); + originalPublish.apply(channel, arguments); + } catch (err) { + done(err); + } + }; + + Ably.Rest.Http.doUri = function (method, rest, uri, headers, body, params, callback) { + originalDoUri(method, rest, uri, headers, body, params, function (err) { + if (err) { + done(err); + callback(err); + return; + } + /* Fake a publish error from realtime */ + callback({ message: 'moo', code: 50300, statusCode: 503 }); + }); + Ably.Rest.Http.doUri = originalDoUri; + }; + + channel.publish([{ name: 'one' }, { name: 'two' }], function (err) { + if (err) { + done(err); + return; } - /* TODO uncomment when idempotent publishing works on sandbox + + channel.history({ direction: 'forwards' }, function (err, page) { + if (err) { + done(err); + return; + } + /* TODO uncomment when idempotent publishing works on sandbox * until then, test with ABLY_ENV=idempotent-dev test.equal(page.items.length, 2, 'Only one message (with two items) should have been published'); */ - test.equal(page.items[0].id, idOne, 'Check message id 1 preserved in history'); - test.equal(page.items[1].id, idTwo, 'Check message id 1 preserved in history'); - test.done(); + try { + expect(page.items[0].id).to.equal(idOne, 'Check message id 1 preserved in history'); + expect(page.items[1].id).to.equal(idTwo, 'Check message id 1 preserved in history'); + done(); + } catch (err) { + done(err); + } + }); }); }); - }; - exports.restpublishpromise = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; + if (typeof Promise !== undefined) { + it('Rest publish promise', function (done) { + var rest = helper.AblyRest({ promises: true }); + var channel = rest.channels.get('publishpromise'); + + channel + .publish('name', 'data') + .then(function () { + return channel.history(); + }) + .then(function (page) { + var message = page.items[0]; + try { + expect( + message.data == 'data', + 'Check publish and history promise methods both worked as expected' + ).to.be.ok; + done(); + } catch (err) { + done(err); + } + }) + ['catch'](function (err) { + done(err); + }); + }); } - test.expect(2); - var rest = helper.AblyRest({promises: true}); - var channel = rest.channels.get('publishpromise'); - - channel.publish('name', 'data').then(function() { - test.ok(true, 'Check publish returns a promise that resolves on publish'); - return channel.history(); - }).then(function(page) { - var message = page.items[0]; - test.ok(message.data == 'data', 'Check publish and history promise methods both worked as expected'); - test.done(); - })['catch'](function(err) { - test.ok(false, 'Promise chain failed with error: ' + displayError(err)); - test.done(); + + it('Rest publish params', function (done) { + var rest = helper.AblyRest(), + channel = rest.channels.get('publish_params'); + + /* Stub out _publish to check params */ + var i = 0; + channel._publish = function (requestBody, headers, params) { + try { + expect(params && params.testParam).to.equal('testParamValue'); + } catch (err) { + done(err); + } + if (++i === 8) { + done(); + } + }; + + channel.publish('foo', 'bar', { testParam: 'testParamValue' }); + channel.publish('foo', { data: 'data' }, { testParam: 'testParamValue' }); + channel.publish('foo', { data: 'data' }, { testParam: 'testParamValue' }, noop); + channel.publish('foo', null, { testParam: 'testParamValue' }); + channel.publish(null, 'foo', { testParam: 'testParamValue' }); + channel.publish({ name: 'foo', data: 'bar' }, { testParam: 'testParamValue' }); + channel.publish([{ name: 'foo', data: 'bar' }], { testParam: 'testParamValue' }); + channel.publish([{ name: 'foo', data: 'bar' }], { testParam: 'testParamValue' }, noop); }); - }; - - exports.restpublishparams = function(test) { - test.expect(8); - var rest = helper.AblyRest(), - channel = rest.channels.get('publish_params'); - - /* Stub out _publish to check params */ - var i = 0; - channel._publish = function(requestBody, headers, params) { - test.equal(params && params.testParam, 'testParamValue'); - if(++i === 8) { - test.done(); - } - }; - - channel.publish('foo', 'bar', {testParam: 'testParamValue'}); - channel.publish('foo', {data: 'data'}, {testParam: 'testParamValue'}); - channel.publish('foo', {data: 'data'}, {testParam: 'testParamValue'}, noop); - channel.publish('foo', null, {testParam: 'testParamValue'}); - channel.publish(null, 'foo', {testParam: 'testParamValue'}); - channel.publish({name: 'foo', data: 'bar'}, {testParam: 'testParamValue'}); - channel.publish([{name: 'foo', data: 'bar'}], {testParam: 'testParamValue'}); - channel.publish([{name: 'foo', data: 'bar'}], {testParam: 'testParamValue'}, noop); - }; - - helper.withMocha('rest/message', exports); -}); \ No newline at end of file + }); +}); diff --git a/spec/rest/presence.test.js b/spec/rest/presence.test.js index 9c62606f81..d35c5a7ae7 100644 --- a/spec/rest/presence.test.js +++ b/spec/rest/presence.test.js @@ -1,141 +1,149 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var currentTime, rest, cipherConfig, exports = {}, - Crypto = Ably.Realtime.Crypto, - BufferUtils = Ably.Realtime.BufferUtils, - displayError = helper.displayError, - arrFind = helper.arrFind, - cipherParamsFromConfig = function(cipherConfig) { - var cipherParams = new Crypto.CipherParams; - for(var prop in cipherConfig) { - cipherParams[prop] = cipherConfig[prop]; - }; - cipherParams.keyLength = cipherConfig.keylength; - delete cipherParams.keylength; // grr case differences - cipherParams.key = BufferUtils.base64Decode(cipherParams.key); - cipherParams.iv = BufferUtils.base64Decode(cipherParams.iv); - return cipherParams; +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var rest; + var cipherConfig; + var expect = chai.expect; + var Crypto = Ably.Realtime.Crypto; + var BufferUtils = Ably.Realtime.BufferUtils; + var arrFind = helper.arrFind; + + function cipherParamsFromConfig(cipherConfig) { + var cipherParams = new Crypto.CipherParams(); + for (var prop in cipherConfig) { + cipherParams[prop] = cipherConfig[prop]; } + cipherParams.keyLength = cipherConfig.keylength; + delete cipherParams.keylength; // grr case differences + cipherParams.key = BufferUtils.base64Decode(cipherParams.key); + cipherParams.iv = BufferUtils.base64Decode(cipherParams.iv); + return cipherParams; + } - exports.before = function(test) { - test.expect(1); - helper.setupApp(function() { - rest = helper.AblyRest(); - cipherConfig = helper.getTestApp().cipherConfig; - test.ok(true, 'Setup REST library'); - test.done(); + describe('rest/presence', function () { + before(function (done) { + helper.setupApp(function () { + rest = helper.AblyRest(); + cipherConfig = helper.getTestApp().cipherConfig; + done(); + }); }); - }; - function presence_simple(operation) { return function(test) { - test.expect(7); - try { - var cipherParams = cipherParamsFromConfig(cipherConfig); - var channel = rest.channels.get('persisted:presence_fixtures', - {cipher: cipherParams}); - channel.presence[operation](function(err, resultPage) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - var presenceMessages = resultPage.items; - test.equal(presenceMessages.length, 6, 'Verify correct number of messages found'); - if(presenceMessages.length != 6) { - console.log('presenceMessages: ', JSON.stringify(presenceMessages)); + function presence_simple(operation) { + return function (done) { + try { + var cipherParams = cipherParamsFromConfig(cipherConfig); + var channel = rest.channels.get('persisted:presence_fixtures', { cipher: cipherParams }); + channel.presence[operation](function (err, resultPage) { + if (err) { + done(err); + return; + } + var presenceMessages = resultPage.items; + expect(presenceMessages.length).to.equal(6, 'Verify correct number of messages found'); + if (presenceMessages.length != 6) { + console.log('presenceMessages: ', JSON.stringify(presenceMessages)); + } + var encodedMessage = arrFind(presenceMessages, function (msg) { + return msg.clientId == 'client_encoded'; + }); + var decodedMessage = arrFind(presenceMessages, function (msg) { + return msg.clientId == 'client_decoded'; + }); + var boolMessage = arrFind(presenceMessages, function (msg) { + return msg.clientId == 'client_bool'; + }); + var intMessage = arrFind(presenceMessages, function (msg) { + return msg.clientId == 'client_int'; + }); + expect(encodedMessage.data).to.deep.equal(decodedMessage.data, 'Verify message decoding works correctly'); + expect(encodedMessage.encoding).to.equal(null, 'Decoding should remove encoding field'); + expect(decodedMessage.encoding).to.equal(null, 'Decoding should remove encoding field'); + expect(boolMessage.data).to.equal('true', 'should not attempt to parse string data when no encoding field'); + expect(intMessage.data).to.equal('24', 'should not attempt to parse string data when no encoding field'); + expect(boolMessage.action).to.equal(operation === 'get' ? 'present' : 'enter', 'appropriate action'); + done(); + }); + } catch (err) { + done(err); } - var encodedMessage = arrFind(presenceMessages, function(msg) {return msg.clientId == 'client_encoded'}); - var decodedMessage = arrFind(presenceMessages, function(msg) {return msg.clientId == 'client_decoded'}); - var boolMessage = arrFind(presenceMessages, function(msg) {return msg.clientId == 'client_bool'}); - var intMessage = arrFind(presenceMessages, function(msg) {return msg.clientId == 'client_int'}); - test.deepEqual(encodedMessage.data, decodedMessage.data, 'Verify message decoding works correctly'); - test.equal(encodedMessage.encoding, null, 'Decoding should remove encoding field'); - test.equal(decodedMessage.encoding, null, 'Decoding should remove encoding field'); - test.equal(boolMessage.data, 'true', 'should not attempt to parse string data when no encoding field'); - test.equal(intMessage.data, '24', 'should not attempt to parse string data when no encoding field'); - test.equal(boolMessage.action, (operation === 'get') ? 'present' : 'enter', 'appropriate action'); - test.done(); - }); - } catch(e) { - console.log(e.stack); + }; } - }} - exports.presence_get_simple = presence_simple('get'); - exports.presence_history_simple = presence_simple('history') + it('Presence get simple', presence_simple('get')); + it('Presence history simple', presence_simple('history')); - /* Ensure that calling JSON strinfigy on the Presence object + /* Ensure that calling JSON strinfigy on the Presence object converts the action string value back to a numeric value which the API requires */ - exports.presence_message_json_serialisation = function(test) { - test.expect(2); - var channel = rest.channels.get('persisted:presence_fixtures'); - channel.presence.get(function(err, resultPage) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - var presenceMessages = resultPage.items; - var presenceBool = arrFind(presenceMessages, function(msg) {return msg.clientId == 'client_bool'}); - test.equal(JSON.parse(JSON.stringify(presenceBool)).action, 1); // present - presenceBool.action = 'leave'; - test.equal(JSON.parse(JSON.stringify(presenceBool)).action, 3); // leave - test.done(); + it('Presence message JSON serialisation', function (done) { + var channel = rest.channels.get('persisted:presence_fixtures'); + channel.presence.get(function (err, resultPage) { + if (err) { + done(err); + return; + } + var presenceMessages = resultPage.items; + var presenceBool = arrFind(presenceMessages, function (msg) { + return msg.clientId == 'client_bool'; + }); + try { + expect(JSON.parse(JSON.stringify(presenceBool)).action).to.equal(1); // present + presenceBool.action = 'leave'; + expect(JSON.parse(JSON.stringify(presenceBool)).action).to.equal(3); // leave + done(); + } catch (err) { + done(err); + } + }); }); - }; - exports.presence_get_limits_and_filtering = function(test) { - try { - var channel = rest.channels.get('persisted:presence_fixtures'); + it('Presence get limits and filtering', function (done) { + try { + var channel = rest.channels.get('persisted:presence_fixtures'); - var tests = [ - // Result limit - function(cb) { - channel.presence.get({limit: 3}, function(err, resultPage) { - if(err) cb(err); - var presenceMessages = resultPage.items; - test.equal(presenceMessages.length, 3, 'Verify correct number of messages found'); - cb(); - }); - }, - // Filter by clientId - function(cb) { - channel.presence.get({clientId: 'client_json'}, function(err, resultPage) { - if(err) cb(err); - var presenceMessages = resultPage.items; - test.equal(presenceMessages.length, 1, 'Verify correct number of messages found'); - cb(); - }); - }, - // Filter by connectionId - function(cb) { - channel.presence.get(function(err, resultPage) { - if(err) cb(err); - channel.presence.get({connectionId: resultPage.items[0].connectionId}, function(err, resultPage) { - if(err) cb(err); + var tests = [ + // Result limit + function (cb) { + channel.presence.get({ limit: 3 }, function (err, resultPage) { + if (err) cb(err); var presenceMessages = resultPage.items; - test.equal(presenceMessages.length, 6, 'Verify correct number of messages found'); + expect(presenceMessages.length).to.equal(3, 'Verify correct number of messages found'); cb(); }); - }); - } - ] - - test.expect(tests.length); - - async.parallel(tests, function(err){ - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.done(); - }); - } catch(e) { - console.log(e.stack); - } - }; + }, + // Filter by clientId + function (cb) { + channel.presence.get({ clientId: 'client_json' }, function (err, resultPage) { + if (err) cb(err); + var presenceMessages = resultPage.items; + expect(presenceMessages.length).to.equal(1, 'Verify correct number of messages found'); + cb(); + }); + }, + // Filter by connectionId + function (cb) { + channel.presence.get(function (err, resultPage) { + if (err) cb(err); + channel.presence.get({ connectionId: resultPage.items[0].connectionId }, function (err, resultPage) { + if (err) cb(err); + var presenceMessages = resultPage.items; + expect(presenceMessages.length).to.equal(6, 'Verify correct number of messages found'); + cb(); + }); + }); + } + ]; - helper.withMocha('rest/presence', exports); -}); \ No newline at end of file + async.parallel(tests, function (err) { + if (err) { + done(err); + return; + } + done(); + }); + } catch (err) { + done(err); + } + }); + }); +}); diff --git a/spec/rest/push.test.js b/spec/rest/push.test.js index 70724b5cab..467693fab4 100644 --- a/spec/rest/push.test.js +++ b/spec/rest/push.test.js @@ -1,515 +1,613 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var Resource = Ably.Rest.Resource, - Utils = Ably.Rest.Utils, - exports = {}, - _exports = {}, - displayError = helper.displayError, - closeAndFinish = helper.closeAndFinish, - defaultHeaders = Utils.defaultPostHeaders('msgpack'), - testDevice = { - id: 'testId', - clientId: 'testClientId', - deviceSecret: 'secret-testId', - platform: 'android', - formFactor: 'phone', - push: { - recipient: { - transportType: 'gcm', - registrationToken: 'xxxxxxxxxxx' - } +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async, chai) { + var Utils = Ably.Rest.Utils; + var exports = {}; + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var testDevice = { + id: 'testId', + clientId: 'testClientId', + deviceSecret: 'secret-testId', + platform: 'android', + formFactor: 'phone', + push: { + recipient: { + transportType: 'gcm', + registrationToken: 'xxxxxxxxxxx' } - }, - testDevice_withoutSecret = { - id: 'testId', - platform: 'android', - formFactor: 'phone', - push: { - recipient: { - transportType: 'gcm', - registrationToken: 'xxxxxxxxxxx' - } + } + }; + var testDevice_withoutSecret = { + id: 'testId', + platform: 'android', + formFactor: 'phone', + push: { + recipient: { + transportType: 'gcm', + registrationToken: 'xxxxxxxxxxx' } - }; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function() { - test.ok(true, 'Setup REST library'); - test.done(); - }); + } }; - exports.push_getSubscriptions = function(test) { - var subscribes = [], deletes = []; - var subsByChannel = {}; - for (var i = 0; i < 5; i++) { (function(i) { - var sub = {channel: 'pushenabled:foo' + ((i % 2) + 1), clientId: 'testClient' + ((i % 3) + 1)}; - if (!subsByChannel[sub.channel]) { - subsByChannel[sub.channel] = []; - } - subsByChannel[sub.channel].push(sub); + describe('rest/push', function () { + this.timeout(60 * 1000); - var rest = helper.AblyRest({clientId: sub.clientId}); - subscribes.push(function(callback) { - rest.push.admin.channelSubscriptions.save(sub, callback); + before(function (done) { + helper.setupApp(function () { + done(); }); - deletes.push(function(callback) { - rest.push.admin.channelSubscriptions.remove(sub, callback); - }); - })(i) } - - var rest = helper.AblyRest(); - - async.series([function(callback) { - async.parallel(subscribes, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.list({channel: 'pushenabled:foo1'}, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.list({channel: 'pushenabled:foo2'}, callback); - }, function(callback) { - async.parallel(deletes, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; - } - includesUnordered(test, untyped(result[1].items), untyped(subsByChannel['pushenabled:foo1'])); - includesUnordered(test, untyped(result[2].items), untyped(subsByChannel['pushenabled:foo2'])); - test.done(); }); - }; - - exports.push_publish = function(test) { - var realtime = helper.AblyRealtime(); - var channel = realtime.channels.get('pushenabled:foo'); - channel.attach(function(err) { - if (err) { - test.ok(false, err.message); - closeAndFinish(test, realtime); - return; + it('Get subscriptions', function (done) { + var subscribes = []; + var deletes = []; + var subsByChannel = {}; + for (var i = 0; i < 5; i++) { + (function (i) { + var sub = { channel: 'pushenabled:foo' + ((i % 2) + 1), clientId: 'testClient' + ((i % 3) + 1) }; + if (!subsByChannel[sub.channel]) { + subsByChannel[sub.channel] = []; + } + subsByChannel[sub.channel].push(sub); + + var rest = helper.AblyRest({ clientId: sub.clientId }); + subscribes.push(function (callback) { + rest.push.admin.channelSubscriptions.save(sub, callback); + }); + deletes.push(function (callback) { + rest.push.admin.channelSubscriptions.remove(sub, callback); + }); + })(i); } - var pushPayload = { - notification: {title: 'Test message', body:'Test message body'}, - data: {foo: 'bar'} - }; - - var baseUri = realtime.baseUri(Ably.Rest.Defaults.getHost(realtime.options)); - var pushRecipient = { - transportType: 'ablyChannel', - channel: 'pushenabled:foo', - ablyKey: realtime.options.key, - ablyUrl: baseUri - }; - - channel.subscribe('__ably_push__', function(msg) { - var receivedPushPayload = JSON.parse(msg.data); - test.deepEqual(receivedPushPayload.data, pushPayload.data); - test.deepEqual(receivedPushPayload.notification.title, pushPayload.notification.title); - test.deepEqual(receivedPushPayload.notification.body, pushPayload.notification.body); - closeAndFinish(test, realtime); - }); + var rest = helper.AblyRest(); - realtime.push.admin.publish(pushRecipient, pushPayload, function(err) { + async.series( + [ + function (callback) { + async.parallel(subscribes, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.list({ channel: 'pushenabled:foo1' }, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.list({ channel: 'pushenabled:foo2' }, callback); + }, + function (callback) { + async.parallel(deletes, callback); + } + ], + function (err, result) { + if (err) { + done(err); + return; + } + try { + testIncludesUnordered(untyped(result[1].items), untyped(subsByChannel['pushenabled:foo1'])); + testIncludesUnordered(untyped(result[2].items), untyped(subsByChannel['pushenabled:foo2'])); + done(); + } catch (err) { + done(err); + } + } + ); + }); + + it('Publish', function (done) { + var realtime = helper.AblyRealtime(); + + var channel = realtime.channels.get('pushenabled:foo'); + channel.attach(function (err) { if (err) { - test.ok(false, err.message); - closeAndFinish(test, realtime); + closeAndFinish(done, realtime, err); + return; } + + var pushPayload = { + notification: { title: 'Test message', body: 'Test message body' }, + data: { foo: 'bar' } + }; + + var baseUri = realtime.baseUri(Ably.Rest.Defaults.getHost(realtime.options)); + var pushRecipient = { + transportType: 'ablyChannel', + channel: 'pushenabled:foo', + ablyKey: realtime.options.key, + ablyUrl: baseUri + }; + + channel.subscribe('__ably_push__', function (msg) { + var receivedPushPayload = JSON.parse(msg.data); + try { + expect(receivedPushPayload.data).to.deep.equal(pushPayload.data); + expect(receivedPushPayload.notification.title).to.deep.equal(pushPayload.notification.title); + expect(receivedPushPayload.notification.body).to.deep.equal(pushPayload.notification.body); + closeAndFinish(done, realtime); + } catch (err) { + closeAndFinish(done, realtime, err); + } + }); + + realtime.push.admin.publish(pushRecipient, pushPayload, function (err) { + if (err) { + closeAndFinish(done, realtime, err); + } + }); }); }); - }; - exports.push_publish_promise = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; - } - var realtime = helper.AblyRealtime({promises: true}); - var channelName = 'pushenabled:publish_promise'; - var channel = realtime.channels.get(channelName); - channel.attach(function(err) { - if (err) { - test.ok(false, err.message); - closeAndFinish(test, realtime); - return; - } + if (typeof Promise !== 'undefined') { + it('Publish promise', function (done) { + var realtime = helper.AblyRealtime({ promises: true }); + var channelName = 'pushenabled:publish_promise'; + var channel = realtime.channels.get(channelName); + channel.attach(function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } - var pushPayload = { - notification: {title: 'Test message', body:'Test message body'}, - data: {foo: 'bar'} - }; - - var baseUri = realtime.baseUri(Ably.Rest.Defaults.getHost(realtime.options)); - var pushRecipient = { - transportType: 'ablyChannel', - channel: 'pushenabled:foo', - ablyKey: realtime.options.key, - ablyUrl: baseUri - }; - - channel.subscribe('__ably_push__', function(msg) { - var receivedPushPayload = JSON.parse(msg.data); - test.deepEqual(receivedPushPayload.data, pushPayload.data); - test.deepEqual(receivedPushPayload.notification.title, pushPayload.notification.title); - test.deepEqual(receivedPushPayload.notification.body, pushPayload.notification.body); - closeAndFinish(test, realtime); - }) - - realtime.push.admin.publish(pushRecipient, pushPayload).then(function() { - closeAndFinish(test, realtime); - })['catch'](function(err) { - test.ok(false, displayError(err)); - closeAndFinish(test, realtime); + var pushPayload = { + notification: { title: 'Test message', body: 'Test message body' }, + data: { foo: 'bar' } + }; + + var baseUri = realtime.baseUri(Ably.Rest.Defaults.getHost(realtime.options)); + var pushRecipient = { + transportType: 'ablyChannel', + channel: 'pushenabled:foo', + ablyKey: realtime.options.key, + ablyUrl: baseUri + }; + + channel.subscribe('__ably_push__', function (msg) { + var receivedPushPayload = JSON.parse(msg.data); + try { + expect(receivedPushPayload.data).to.deep.equal(pushPayload.data); + expect(receivedPushPayload.notification.title).to.deep.equal(pushPayload.notification.title); + expect(receivedPushPayload.notification.body).to.deep.equal(pushPayload.notification.body); + closeAndFinish(done, realtime, err); + } catch (err) { + done(err); + } + }); + + realtime.push.admin + .publish(pushRecipient, pushPayload) + .then(function () { + closeAndFinish(done, realtime); + }) + ['catch'](function (err) { + closeAndFinish(done, realtime, err); + }); + }); }); + } + + it('deviceRegistrations save', function (done) { + var rest = helper.AblyRest(); + + async.series( + [ + function (callback) { + rest.push.admin.deviceRegistrations.save(testDevice, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.get(testDevice.id, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.remove(testDevice.id, callback); + } + ], + function (err, result) { + if (err) { + done(err); + return; + } + var saved = result[0]; + var got = result[1]; + try { + expect(got.push.state).to.equal('ACTIVE'); + delete got.metadata; // Ignore these properties for testing + delete got.push.state; + testIncludesUnordered(untyped(got), testDevice_withoutSecret); + testIncludesUnordered(untyped(saved), testDevice_withoutSecret); + done(); + } catch (err) { + done(err); + } + } + ); }); - }; - exports.push_deviceRegistrations_save = function(test) { - var rest = helper.AblyRest(); - - async.series([function(callback) { - rest.push.admin.deviceRegistrations.save(testDevice, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.get(testDevice.id, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.remove(testDevice.id, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; + it('deviceRegistrations get and list', function (done) { + var registrations = []; + var deletes = []; + var devices = []; + var devices_withoutSecret = []; + var devicesByClientId = {}; + var numberOfDevices = 5; + for (var i = 0; i < numberOfDevices; i++) { + (function (i) { + var device = { + id: 'device' + (i + 1), + deviceSecret: 'secret-device' + (i + 1), + clientId: 'testClient' + ((i % 2) + 1), + platform: 'android', + formFactor: 'phone', + push: { + recipient: { + transportType: 'gcm', + registrationToken: 'xxxxxxxxxxx' + } + } + }; + var device_withoutSecret = { + id: 'device' + (i + 1), + clientId: 'testClient' + ((i % 2) + 1), + platform: 'android', + formFactor: 'phone', + push: { + recipient: { + transportType: 'gcm', + registrationToken: 'xxxxxxxxxxx' + } + } + }; + if (!devicesByClientId[device.clientId]) { + devicesByClientId[device.clientId] = []; + } + devicesByClientId[device.clientId].push(device_withoutSecret); + devices.push(device); + devices_withoutSecret.push(device_withoutSecret); + + var rest = helper.AblyRest({ clientId: device.clientId }); + registrations.push(function (callback) { + rest.push.admin.deviceRegistrations.save(device, callback); + }); + deletes.push(function (callback) { + rest.push.admin.deviceRegistrations.remove('device' + (i + 1), callback); + }); + })(i); } - var saved = result[0]; - var got = result[1]; - test.equal(got.push.state, 'ACTIVE'); - delete got.metadata; // Ignore these properties for testing - delete got.push.state; - includesUnordered(test, untyped(got), testDevice_withoutSecret); - includesUnordered(test, untyped(saved), testDevice_withoutSecret); - test.done(); - }); - }; - exports.push_deviceRegistrations_get_and_list = function(test) { - var registrations = []; - var deletes = []; - var devices = []; - var devices_withoutSecret = []; - var devicesByClientId = {}; - var numberOfDevices = 5; - for (var i = 0; i < numberOfDevices; i++) { (function(i) { - var device = { - id: 'device' + (i + 1), - deviceSecret: 'secret-device' + (i + 1), - clientId: 'testClient' + ((i % 2) + 1), - platform: 'android', - formFactor: 'phone', - push: { - recipient: { - transportType: 'gcm', - registrationToken: 'xxxxxxxxxxx' + var rest = helper.AblyRest(); + + async.series( + [ + function (callback) { + async.parallel(registrations, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.list(null, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.list({ clientId: 'testClient1' }, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.list({ clientId: 'testClient2' }, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.get(devices[0].id, callback); + }, + function (callback) { + async.parallel( + [ + function (callback) { + rest.push.admin.deviceRegistrations.removeWhere({ clientId: 'testClient1' }, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.removeWhere({ clientId: 'testClient2' }, callback); + } + ], + callback + ); + }, + function (callback) { + async.parallel(deletes, callback); } - } - }; - var device_withoutSecret = { - id: 'device' + (i + 1), - clientId: 'testClient' + ((i % 2) + 1), - platform: 'android', - formFactor: 'phone', - push: { - recipient: { - transportType: 'gcm', - registrationToken: 'xxxxxxxxxxx' + ], + function (err, result) { + if (err) { + done(err); + return; + } + try { + expect(numberOfDevices).to.equal(result[0].length); + testIncludesUnordered(untyped(result[1].items), untyped(devices_withoutSecret)); + testIncludesUnordered(untyped(result[2].items), untyped(devicesByClientId['testClient1'])); + testIncludesUnordered(untyped(result[3].items), untyped(devicesByClientId['testClient2'])); + testIncludesUnordered(untyped(result[4]), untyped(devices[0])); + done(); + } catch (err) { + done(err); } } - }; - if (!devicesByClientId[device.clientId]) { - devicesByClientId[device.clientId] = []; - } - devicesByClientId[device.clientId].push(device_withoutSecret); - devices.push(device); - devices_withoutSecret.push(device_withoutSecret); + ); + }); - var rest = helper.AblyRest({clientId: device.clientId}); - registrations.push(function(callback) { - rest.push.admin.deviceRegistrations.save(device, callback); - }); - deletes.push(function(callback) { - rest.push.admin.deviceRegistrations.remove('device' + (i + 1), callback); - }); - })(i) } - - var rest = helper.AblyRest(); - - async.series([function(callback) { - async.parallel(registrations, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.list(null, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.list({clientId: 'testClient1'}, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.list({clientId: 'testClient2'}, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.get(devices[0].id, callback); - }, function(callback) { - async.parallel([function(callback) { - rest.push.admin.deviceRegistrations.removeWhere({clientId: 'testClient1'}, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.removeWhere({clientId: 'testClient2'}, callback); - }], callback); - }, function(callback) { - async.parallel(deletes, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; - } - test.equal(numberOfDevices, result[0].length); - includesUnordered(test, untyped(result[1].items), untyped(devices_withoutSecret)); - includesUnordered(test, untyped(result[2].items), untyped(devicesByClientId['testClient1'])); - includesUnordered(test, untyped(result[3].items), untyped(devicesByClientId['testClient2'])); - includesUnordered(test, untyped(result[4]), untyped(devices[0])); - test.done(); + it('deviceRegistrations remove removeWhere', function (done) { + var rest = helper.AblyRest(); + + async.series( + [ + function (callback) { + rest.push.admin.deviceRegistrations.save(testDevice, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.remove(testDevice.id, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.get(testDevice.id, function (err, result) { + expect(err && err.statusCode).to.equal(404, 'Check device reg not found after removal'); + callback(null); + }); + }, + function (callback) { + rest.push.admin.deviceRegistrations.save(testDevice, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.removeWhere({ deviceId: testDevice.id }, callback); + }, + function (callback) { + rest.push.admin.deviceRegistrations.get(testDevice.id, function (err, result) { + expect(err && err.statusCode).to.equal(404, 'Check device reg not found after removal'); + callback(null); + }); + } + ], + function (err, result) { + if (err) { + done(err); + } + done(); + } + ); }); - }; - exports.push_deviceRegistrations_remove_removeWhere = function(test) { - var rest = helper.AblyRest(); - - async.series([ - function(callback) { - rest.push.admin.deviceRegistrations.save(testDevice, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.remove(testDevice.id, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.get(testDevice.id, function(err, result) { - test.equal(err && err.statusCode, 404, 'Check device reg not found after removal'); - callback(null); - }); - }, - function(callback) { - rest.push.admin.deviceRegistrations.save(testDevice, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.removeWhere({deviceId: testDevice.id}, callback); - }, function(callback) { - rest.push.admin.deviceRegistrations.get(testDevice.id, function(err, result) { - test.equal(err && err.statusCode, 404, 'Check device reg not found after removal'); - callback(null); + if (typeof Promise !== undefined) { + it('deviceRegistrations promise', function (done) { + var rest = helper.AblyRest({ promises: true }); + + /* save */ + rest.push.admin.deviceRegistrations + .save(testDevice) + .then(function (saved) { + expect(saved.push.state).to.equal('ACTIVE'); + testIncludesUnordered(untyped(saved), testDevice_withoutSecret); + /* get */ + return rest.push.admin.deviceRegistrations.get(testDevice.id); + }) + .then(function (got) { + expect(got.push.state).to.equal('ACTIVE'); + delete got.metadata; // Ignore these properties for testing + delete got.push.state; + testIncludesUnordered(untyped(got), testDevice_withoutSecret); + /* list */ + return rest.push.admin.deviceRegistrations.list({ clientId: testDevice.clientId }); + }) + .then(function (result) { + expect(result.items.length).to.equal(1); + var got = result.items[0]; + expect(got.push.state).to.equal('ACTIVE'); + testIncludesUnordered(untyped(got), testDevice_withoutSecret); + /* remove */ + return rest.push.admin.deviceRegistrations.removeWhere({ deviceId: testDevice.id }); + }) + .then(function () { + done(); + }) + ['catch'](function (err) { + done(err); + }); }); } - ], function(err, result) { - if(err) { - test.ok(false, displayError(err)); - } - test.done(); - }); - }; - exports.push_deviceRegistrations_promise = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; - } - var rest = helper.AblyRest({promises: true}); - - /* save */ - rest.push.admin.deviceRegistrations.save(testDevice).then(function(saved) { - test.equal(saved.push.state, 'ACTIVE'); - includesUnordered(test, untyped(saved), testDevice_withoutSecret); - /* get */ - return rest.push.admin.deviceRegistrations.get(testDevice.id); - }).then(function(got) { - test.equal(got.push.state, 'ACTIVE'); - delete got.metadata; // Ignore these properties for testing - delete got.push.state; - includesUnordered(test, untyped(got), testDevice_withoutSecret); - /* list */ - return rest.push.admin.deviceRegistrations.list({clientId: testDevice.clientId}); - }).then(function(result) { - test.equal(result.items.length, 1); - var got = result.items[0]; - test.equal(got.push.state, 'ACTIVE'); - includesUnordered(test, untyped(got), testDevice_withoutSecret); - /* remove */ - return rest.push.admin.deviceRegistrations.removeWhere({deviceId: testDevice.id}); - }).then(function() { - test.done(); - })['catch'](function(err) { - test.ok(false, displayError(err)); - test.done(); + it('channelSubscriptions save', function (done) { + var rest = helper.AblyRest({ clientId: 'testClient' }); + var subscription = { clientId: 'testClient', channel: 'pushenabled:foo' }; + + async.series( + [ + function (callback) { + rest.push.admin.channelSubscriptions.save(subscription, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.list({ channel: 'pushenabled:foo' }, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.remove(subscription, callback); + } + ], + function (err, result) { + if (err) { + done(err); + return; + } + var saved = result[0]; + var sub = result[1].items[0]; + try { + expect(subscription.clientId).to.equal(saved.clientId); + expect(subscription.channel).to.equal(saved.channel); + expect(subscription.clientId).to.equal(sub.clientId); + expect(subscription.channel).to.equal(sub.channel); + done(); + } catch (err) { + done(err); + } + } + ); }); - }; - exports.push_channelSubscriptions_save = function(test) { - var rest = helper.AblyRest({clientId: 'testClient'}); - var subscription = {clientId: 'testClient', channel: 'pushenabled:foo'}; - - async.series([function(callback) { - rest.push.admin.channelSubscriptions.save(subscription, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.list({channel: 'pushenabled:foo'}, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.remove(subscription, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; + it('channelSubscriptions get', function (done) { + var subscribes = []; + var deletes = []; + var subsByChannel = {}; + for (var i = 0; i < 5; i++) { + (function (i) { + var sub = { channel: 'pushenabled:foo' + ((i % 2) + 1), clientId: 'testClient' + i }; + if (!subsByChannel[sub.channel]) { + subsByChannel[sub.channel] = []; + } + subsByChannel[sub.channel].push(sub); + + var rest = helper.AblyRest(); + subscribes.push(function (callback) { + rest.push.admin.channelSubscriptions.save(sub, callback); + }); + deletes.push(function (callback) { + rest.push.admin.channelSubscriptions.remove({ clientId: 'testClient' + i }, callback); + }); + })(i); } - var saved = result[0]; - var sub = result[1].items[0]; - test.equal(subscription.clientId, saved.clientId); - test.equal(subscription.channel, saved.channel); - test.equal(subscription.clientId, sub.clientId); - test.equal(subscription.channel, sub.channel); - test.done(); + + var rest = helper.AblyRest(); + + async.series( + [ + function (callback) { + async.parallel(subscribes, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.list({ channel: 'pushenabled:foo1' }, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.list({ channel: 'pushenabled:foo2' }, callback); + }, + function (callback) { + async.parallel(deletes, callback); + } + ], + function (err, result) { + if (err) { + done(err); + return; + } + try { + testIncludesUnordered(untyped(result[1].items), untyped(subsByChannel['pushenabled:foo1'])); + testIncludesUnordered(untyped(result[2].items), untyped(subsByChannel['pushenabled:foo2'])); + done(); + } catch (err) { + done(err); + } + } + ); }); - }; - exports.push_channelSubscriptions_get = function(test) { - var subscribes = []; - var deletes = []; - var subsByChannel = {}; - for (var i = 0; i < 5; i++) { (function(i) { - var sub = {channel: 'pushenabled:foo' + ((i % 2) + 1), clientId: 'testClient' + i}; - if (!subsByChannel[sub.channel]) { - subsByChannel[sub.channel] = []; + exports.push_channelSubscriptions_remove = function (test) { + var rest = helper.AblyRest({ clientId: 'testClient' }); + var subscription = { clientId: 'testClient', channel: 'pushenabled:foo' }; + + async.series( + [ + function (callback) { + rest.push.admin.channelSubscriptions.save(subscription, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.remove(subscription, callback); + } + ], + function (err, result) { + if (err) { + test.ok(false, err.message); + test.done(); + return; + } + test.done(); + } + ); + }; + + it('channelSubscriptions listChannels', function (done) { + var subscribes = []; + var deletes = []; + for (var i = 0; i < 5; i++) { + (function (i) { + var sub = { channel: 'pushenabled:listChannels' + ((i % 2) + 1), clientId: 'testClient' + ((i % 3) + 1) }; + var rest = helper.AblyRest({ clientId: sub.clientId }); + subscribes.push(function (callback) { + rest.push.admin.channelSubscriptions.save(sub, callback); + }); + deletes.push(function (callback) { + rest.push.admin.channelSubscriptions.remove(sub, callback); + }); + })(i); } - subsByChannel[sub.channel].push(sub); var rest = helper.AblyRest(); - subscribes.push(function(callback) { - rest.push.admin.channelSubscriptions.save(sub, callback); - }); - deletes.push(function(callback) { - rest.push.admin.channelSubscriptions.remove({clientId: 'testClient' + i}, callback); - }); - })(i) } - - var rest = helper.AblyRest(); - - async.series([function(callback) { - async.parallel(subscribes, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.list({channel: 'pushenabled:foo1'}, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.list({channel: 'pushenabled:foo2'}, callback); - }, function(callback) { - async.parallel(deletes, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; - } - includesUnordered(test, untyped(result[1].items), untyped(subsByChannel['pushenabled:foo1'])); - includesUnordered(test, untyped(result[2].items), untyped(subsByChannel['pushenabled:foo2'])); - test.done(); - }); - }; - exports.push_channelSubscriptions_remove = function(test) { - var rest = helper.AblyRest({clientId: 'testClient'}); - var subscription = {clientId: 'testClient', channel: 'pushenabled:foo'}; - - async.series([function(callback) { - rest.push.admin.channelSubscriptions.save(subscription, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.remove(subscription, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; - } - test.done(); + async.series( + [ + function (callback) { + async.parallel(subscribes, callback); + }, + function (callback) { + rest.push.admin.channelSubscriptions.listChannels(null, callback); + }, + function (callback) { + async.parallel(deletes, callback); + } + ], + function (err, result) { + if (err) { + done(err); + return; + } + try { + testIncludesUnordered(['pushenabled:listChannels1', 'pushenabled:listChannels2'], result[1].items); + done(); + } catch (err) { + done(err); + } + } + ); }); - }; - exports.push_channelSubscriptions_listChannels = function(test) { - var subscribes = []; - var deletes = []; - for (var i = 0; i < 5; i++) { (function(i) { - var sub = {channel: 'pushenabled:listChannels' + ((i % 2) + 1), clientId: 'testClient' + ((i % 3) + 1)}; - var rest = helper.AblyRest({clientId: sub.clientId}); - subscribes.push(function(callback) { - rest.push.admin.channelSubscriptions.save(sub, callback); + if (typeof Promise !== 'undefined') { + it('channelSubscriptions promise', function (done) { + var rest = helper.AblyRest({ promises: true }); + var channelId = 'pushenabled:channelsubscriptions_promise'; + var subscription = { clientId: 'testClient', channel: channelId }; + + rest.push.admin.channelSubscriptions + .save(subscription) + .then(function (saved) { + expect(subscription.clientId).to.equal(saved.clientId); + expect(subscription.channel).to.equal(saved.channel); + return rest.push.admin.channelSubscriptions.list({ channel: channelId }); + }) + .then(function (result) { + var sub = result.items[0]; + expect(subscription.clientId).to.equal(sub.clientId); + expect(subscription.channel).to.equal(sub.channel); + return rest.push.admin.channelSubscriptions.listChannels(null); + }) + .then(function (result) { + expect(Utils.arrIn(result.items, channelId)).to.be.ok; + return rest.push.admin.channelSubscriptions.remove(subscription); + }) + .then(function () { + done(); + }) + ['catch'](function (err) { + done(err); + }); }); - deletes.push(function(callback) { - rest.push.admin.channelSubscriptions.remove(sub, callback); - }); - })(i) } - - var rest = helper.AblyRest(); - - async.series([function(callback) { - async.parallel(subscribes, callback); - }, function(callback) { - rest.push.admin.channelSubscriptions.listChannels(null, callback); - }, function(callback) { - async.parallel(deletes, callback); - }], function(err, result) { - if (err) { - test.ok(false, err.message); - test.done(); - return; - } - includesUnordered(test, ['pushenabled:listChannels1', 'pushenabled:listChannels2'], result[1].items); - test.done(); - }); - }; - - exports.push_channelSubscriptions_promise = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; } - var rest = helper.AblyRest({promises: true}); - var channelId = 'pushenabled:channelsubscriptions_promise'; - var subscription = {clientId: 'testClient', channel: channelId}; - - rest.push.admin.channelSubscriptions.save(subscription).then(function(saved) { - test.equal(subscription.clientId, saved.clientId); - test.equal(subscription.channel, saved.channel); - return rest.push.admin.channelSubscriptions.list({channel: channelId}); - }).then(function(result) { - var sub = result.items[0]; - test.equal(subscription.clientId, sub.clientId); - test.equal(subscription.channel, sub.channel); - return rest.push.admin.channelSubscriptions.listChannels(null); - }).then(function(result) { - test.ok(Utils.arrIn(result.items, channelId)); - return rest.push.admin.channelSubscriptions.remove(subscription); - }).then(function() { - test.done(); - })['catch'](function(err) { - test.ok(false, displayError(err)); - test.done(); - }); - }; - function untyped(x) { - return JSON.parse(JSON.stringify(x)); - } - - /** - * Tests whether x includes y: equal primitives, x's objects include y's - * objects, x's array elements include y's array elements disregarding - * order. - * - * includesUnordered(x, y) -> string | true - * includesUnordered(test, x, y) -> void - */ - function includesUnordered() { - if (arguments.length == 2) { - var x = arguments[0]; - var y = arguments[1]; + function untyped(x) { + return JSON.parse(JSON.stringify(x)); + } + /** + * Returns true when x includes y: equal primitives, x's objects include y's + * objects, x's array elements include y's array elements disregarding + * order. + * + * includesUnordered(x, y) -> string | true + */ + function includesUnordered(x, y) { if (Utils.isArray(x)) { if (!Utils.isArray(y)) { return 'not both arrays'; @@ -536,7 +634,7 @@ define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { } } if (!found) { - var eq = "couldn't find matching element for " + i + "-th element: \n"; + var eq = "couldn't find matching element for " + i + '-th element: \n'; for (var i in results) { eq += i + '. ' + results[i] + '\n'; } @@ -566,13 +664,12 @@ define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { return x == y ? true : 'primitives not equal'; } - var test = arguments[0]; - var x = arguments[1]; - var y = arguments[2]; - - var eq = includesUnordered(x, y); - test.ok(eq === true, JSON.stringify(x, null, 2) + ' includesUnordered ' + JSON.stringify(y, null, 2) + ' (' + eq + ')'); - } - - helper.withMocha('rest/push', exports); -}); \ No newline at end of file + function testIncludesUnordered(x, y) { + var eq = includesUnordered(x, y); + expect(eq).to.equal( + true, + JSON.stringify(x, null, 2) + ' includesUnordered ' + JSON.stringify(y, null, 2) + ' (' + eq + ')' + ); + } + }); +}); diff --git a/spec/rest/request.test.js b/spec/rest/request.test.js index 0884eeb363..7569d0a409 100644 --- a/spec/rest/request.test.js +++ b/spec/rest/request.test.js @@ -1,194 +1,241 @@ -"use strict"; - -define(['ably', 'shared_helper', 'async'], function(Ably, helper, async) { - var rest, - exports = {}, - _exports = {}, - utils = helper.Utils, - echoServerHost = 'echo.ably.io', - restTestOnJsonMsgpack = helper.restTestOnJsonMsgpack; - - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - - rest = helper.AblyRest({ useBinaryProtocol: false }); - test.ok(true, 'app set up'); - test.done(); +'use strict'; + +define(['shared_helper', 'async', 'chai'], function (helper, async, chai) { + var rest; + var expect = chai.expect; + var utils = helper.Utils; + var echoServerHost = 'echo.ably.io'; + var restTestOnJsonMsgpack = helper.restTestOnJsonMsgpack; + + describe('rest/request', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + rest = helper.AblyRest({ useBinaryProtocol: false }); + done(); + }); }); - }; - restTestOnJsonMsgpack(exports, 'request_time', function(test, rest) { - rest.request('get', '/time', null, null, null, function(err, res) { - test.ok(!err, err && helper.displayError(err)); - test.equal(res.statusCode, 200, 'Check statusCode'); - test.equal(res.success, true, 'Check success'); - test.ok(utils.isArray(res.items), true, 'Check array returned'); - test.equal(res.items.length, 1, 'Check array was of length 1'); - test.done(); + restTestOnJsonMsgpack('request_time', function (done, rest) { + rest.request('get', '/time', null, null, null, function (err, res) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(res.statusCode).to.equal(200, 'Check statusCode'); + expect(res.success).to.equal(true, 'Check success'); + expect(utils.isArray(res.items), true, 'Check array returned').to.be.ok; + expect(res.items.length).to.equal(1, 'Check array was of length 1'); + done(); + } catch (err) { + done(err); + } + }); }); - }); - restTestOnJsonMsgpack(exports, 'request_404', function(test, rest) { - /* NB: can't just use /invalid or something as the CORS preflight will - * fail. Need something superficially a valid path but where the actual - * request fails */ - rest.request('get', '/keys/ablyjs.test/requestToken', null, null, null, function(err, res) { - test.equal(err, null, 'Check that we do not get an error response for a failure that returns an actual ably error code'); - test.equal(res.success, false, 'Check res.success is false for a failure'); - test.equal(res.statusCode, 404, 'Check HPR.statusCode is 404'); - test.equal(res.errorCode, 40400, 'Check HPR.errorCode is 40400'); - test.ok(res.errorMessage, 'Check have an HPR.errorMessage'); - test.done(); - }); - }); - - restTestOnJsonMsgpack(exports, 'request_404', function(test, rest) { - /* NB: can't just use /invalid or something as the CORS preflight will - * fail. Need something superficially a valid path but where the actual - * request fails */ - rest.request('get', '/keys/ablyjs.test/requestToken', null, null, null, function(err, res) { - test.equal(err, null, 'Check that we do not get an error response for a failure that returns an actual ably error code'); - test.equal(res.success, false, 'Check res.success is false for a failure'); - test.equal(res.statusCode, 404, 'Check HPR.statusCode is 404'); - test.equal(res.errorCode, 40400, 'Check HPR.errorCode is 40400'); - test.ok(res.errorMessage, 'Check have an HPR.errorMessage'); - test.done(); + restTestOnJsonMsgpack('request_404', function (done, rest) { + /* NB: can't just use /invalid or something as the CORS preflight will + * fail. Need something superficially a valid path but where the actual + * request fails */ + rest.request('get', '/keys/ablyjs.test/requestToken', null, null, null, function (err, res) { + try { + expect(err).to.equal( + null, + 'Check that we do not get an error response for a failure that returns an actual ably error code' + ); + expect(res.success).to.equal(false, 'Check res.success is false for a failure'); + expect(res.statusCode).to.equal(404, 'Check HPR.statusCode is 404'); + expect(res.errorCode).to.equal(40400, 'Check HPR.errorCode is 40400'); + expect(res.errorMessage, 'Check have an HPR.errorMessage').to.be.ok; + done(); + } catch (err) { + done(err); + } + }); }); - }); - /* With a network issue, should get an actual err, not an HttpPaginatedResponse with error members */ - exports.request_network_error = function(test) { - rest = helper.AblyRest({restHost: helper.unroutableAddress}) - rest.request('get', '/time', null, null, null, function(err, res) { - test.ok(err, 'Check get an err'); - test.ok(!res, 'Check do not get a res'); - test.done(); + /* With a network issue, should get an actual err, not an HttpPaginatedResponse with error members */ + it('request_network_error', function (done) { + rest = helper.AblyRest({ restHost: helper.unroutableAddress }); + rest.request('get', '/time', null, null, null, function (err, res) { + try { + expect(err, 'Check get an err').to.be.ok; + expect(!res, 'Check do not get a res').to.be.ok; + done(); + } catch (err) { + done(err); + } + }); }); - }; - - /* Use the request feature to publish, then retrieve (one at a time), some messages */ - restTestOnJsonMsgpack(exports, 'request_post_get_messages', function(test, rest, channelName) { - test.expect(23); - var channelPath = '/channels/' + channelName + '/messages', - msgone = {name: 'faye', data: 'whittaker'}, - msgtwo = {name: 'martin', data: 'reed'}; - - async.waterfall([ - function(cb) { - rest.request('post', channelPath, null, msgone, null, function(err, res) { - test.ok(!err, err && helper.displayError(err)); - test.equal(res.statusCode, 201, 'Check statusCode is 201'); - test.equal(res.success, true, 'Check post was a success'); - test.equal(res.items && res.items.length, 1, 'Check number of results is as expected'); - cb(); - }); - }, - function(cb) { - rest.request('post', channelPath, null, msgtwo, null, function(err, res) { - test.ok(!err, err && helper.displayError(err)); - test.equal(res.statusCode, 201, 'Check statusCode is 201'); - test.equal(res.items && res.items.length, 1, 'Check number of results is as expected'); - cb(); - }); - }, - function(cb) { - rest.request('get', channelPath, {limit: 1, direction: 'forwards'}, null, null, function(err, res) { - test.ok(!err, err && helper.displayError(err)); - test.equal(res.statusCode, 200, 'Check statusCode is 200'); - test.equal(res.items.length, 1, 'Check only one msg returned'); - test.equal(res.items[0].name, msgone.name, 'Check name is as expected'); - test.equal(res.items[0].data, msgone.data, 'Check data is as expected'); - test.ok(res.hasNext, 'Check hasNext is true'); - cb(null, res.next); - }); - }, - function(next, cb) { - next(function(err, res) { - test.ok(!err, err && helper.displayError(err)); - test.equal(res.statusCode, 200, 'Check statusCode is 200'); - test.equal(res.success, true, 'Check success'); - test.equal(res.items.length, 1, 'Check only one msg returned'); - test.equal(res.items[0].name, msgtwo.name, 'Check name is as expected'); - test.equal(res.items[0].data, msgtwo.data, 'Check data is as expected'); - cb(); - }); - }, - function(cb) { - /* Finally check the messages the 'normal' way to make sure everything's as expected */ - rest.channels.get(channelName).history(function(err, res) { - test.ok(!err, err && helper.displayError(err)); - test.equal(res.items.length, 2, 'Check both msgs returned'); - test.equal(res.items[0].name, msgtwo.name, 'Check name is as expected'); - test.equal(res.items[0].data, msgtwo.data, 'Check data is as expected'); - cb(); - }); - } - ], function() { - test.done(); - }) - }); - restTestOnJsonMsgpack(exports, 'request_batch_api_success', function(test, rest, name) { - var body = {channels: [name + '1', name + '2'], messages: {data: 'foo'}}; - - rest.request("POST", "/messages", {}, body, {}, function(err, res) { - test.equal(err, null, 'Check that we do not get an error response for a success'); - test.equal(res.success, true, 'Check res.success is true for a success'); - test.equal(res.statusCode, 201, 'Check res.statusCode is 201 for a success'); - test.equal(res.errorCode, null, 'Check res.errorCode is null for a success'); - test.equal(res.errorMessage, null, 'Check res.errorMessage is null for a success'); - - test.ok(!res.items[0].batchResponse, 'Check no batchResponse, since items is now just a flat array of channel responses'); - test.equal(res.items.length, 2, 'Verify batched response includes response for each channel'); - test.ok(!res.items[0].error, 'Verify channel1 response is not an error'); - test.equal(res.items[0].channel, name + '1', 'Verify channel1 response includes correct channel'); - test.ok(!res.items[1].error, 'Verify channel2 response is not an error'); - test.equal(res.items[1].channel, name + '2', 'Verify channel2 response includes correct channel'); - test.done(); + /* Use the request feature to publish, then retrieve (one at a time), some messages */ + restTestOnJsonMsgpack('request_post_get_messages', function (done, rest, channelName) { + var channelPath = '/channels/' + channelName + '/messages', + msgone = { name: 'faye', data: 'whittaker' }, + msgtwo = { name: 'martin', data: 'reed' }; + + async.waterfall( + [ + function (cb) { + rest.request('post', channelPath, null, msgone, null, function (err, res) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(res.statusCode).to.equal(201, 'Check statusCode is 201'); + expect(res.success).to.equal(true, 'Check post was a success'); + expect(res.items && res.items.length).to.equal(1, 'Check number of results is as expected'); + cb(); + } catch (err) { + cb(err); + } + }); + }, + function (cb) { + rest.request('post', channelPath, null, msgtwo, null, function (err, res) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(res.statusCode).to.equal(201, 'Check statusCode is 201'); + expect(res.items && res.items.length).to.equal(1, 'Check number of results is as expected'); + cb(); + } catch (err) { + cb(err); + } + }); + }, + function (cb) { + rest.request('get', channelPath, { limit: 1, direction: 'forwards' }, null, null, function (err, res) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(res.statusCode).to.equal(200, 'Check statusCode is 200'); + expect(res.items.length).to.equal(1, 'Check only one msg returned'); + expect(res.items[0].name).to.equal(msgone.name, 'Check name is as expected'); + expect(res.items[0].data).to.equal(msgone.data, 'Check data is as expected'); + expect(res.hasNext, 'Check hasNext is true').to.be.ok; + cb(null, res.next); + } catch (err) { + cb(err); + } + }); + }, + function (next, cb) { + next(function (err, res) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(res.statusCode).to.equal(200, 'Check statusCode is 200'); + expect(res.success).to.equal(true, 'Check success'); + expect(res.items.length).to.equal(1, 'Check only one msg returned'); + expect(res.items[0].name).to.equal(msgtwo.name, 'Check name is as expected'); + expect(res.items[0].data).to.equal(msgtwo.data, 'Check data is as expected'); + cb(); + } catch (err) { + cb(err); + } + }); + }, + function (cb) { + /* Finally check the messages the 'normal' way to make sure everything's as expected */ + rest.channels.get(channelName).history(function (err, res) { + try { + expect(!err, err && helper.displayError(err)).to.be.ok; + expect(res.items.length).to.equal(2, 'Check both msgs returned'); + expect(res.items[0].name).to.equal(msgtwo.name, 'Check name is as expected'); + expect(res.items[0].data).to.equal(msgtwo.data, 'Check data is as expected'); + cb(); + } catch (err) { + cb(err); + } + }); + } + ], + function (err) { + if (err) { + done(err); + return; + } + done(); + } + ); }); - }); - - restTestOnJsonMsgpack(exports, 'request_batch_api_partial_success', function(test, rest, name) { - var body = {channels: [name, '[invalid', ''], messages: {data: 'foo'}}; - rest.request("POST", "/messages", {}, body, {}, function(err, res) { - test.equal(err, null, 'Check that we do not get an error response for a partial success'); - test.equal(res.success, false, 'Check res.success is false for a partial failure'); - test.equal(res.statusCode, 400, 'Check HPR.statusCode is 400 for a partial failure'); - test.equal(res.errorCode, 40020, 'Check HPR.errorCode is 40020 for a partial failure'); - test.ok(res.errorMessage, 'Check have an HPR.errorMessage'); - - var response = res.items[0]; - test.equal(response.error.code, 40020, 'Verify response has an errorCode'); - test.equal(response.batchResponse.length, 3, 'Verify batched response includes response for each channel'); - test.equal(response.batchResponse[0].channel, name, 'Verify channel1 response includes correct channel'); - test.ok(!response.batchResponse[0].error, 'Verify first channel response is not an error'); - test.equal(response.batchResponse[1].error.code, 40010, 'Verify [invalid response includes an error with the right code'); - test.equal(response.batchResponse[2].error.code, 40010, 'Verify empty channel response includes an error with the right code'); - test.done(); + restTestOnJsonMsgpack('request_batch_api_success', function (done, rest, name) { + var body = { channels: [name + '1', name + '2'], messages: { data: 'foo' } }; + + rest.request('POST', '/messages', {}, body, {}, function (err, res) { + try { + expect(err).to.equal(null, 'Check that we do not get an error response for a success'); + expect(res.success).to.equal(true, 'Check res.success is true for a success'); + expect(res.statusCode).to.equal(201, 'Check res.statusCode is 201 for a success'); + expect(res.errorCode).to.equal(null, 'Check res.errorCode is null for a success'); + expect(res.errorMessage).to.equal(null, 'Check res.errorMessage is null for a success'); + + expect( + !res.items[0].batchResponse, + 'Check no batchResponse, since items is now just a flat array of channel responses' + ).to.be.ok; + expect(res.items.length).to.equal(2, 'Verify batched response includes response for each channel'); + expect(!res.items[0].error, 'Verify channel1 response is not an error').to.be.ok; + expect(res.items[0].channel).to.equal(name + '1', 'Verify channel1 response includes correct channel'); + expect(!res.items[1].error, 'Verify channel2 response is not an error').to.be.ok; + expect(res.items[1].channel).to.equal(name + '2', 'Verify channel2 response includes correct channel'); + done(); + } catch (err) { + done(err); + } + }); }); - }); - utils.arrForEach(['put', 'patch', 'delete'], function(method) { - exports['check' + method] = function(test) { - test.expect(1); - var restEcho = helper.AblyRest({ useBinaryProtocol: false, restHost: echoServerHost, tls: true }); - restEcho.request(method, "/methods", {}, {}, {}, function(err, res) { - if(err) { - test.ok(false, helper.displayError(err)); - } else { - test.equal(res.items[0] && res.items[0].method, method); + restTestOnJsonMsgpack('request_batch_api_partial_success', function (done, rest, name) { + var body = { channels: [name, '[invalid', ''], messages: { data: 'foo' } }; + + rest.request('POST', '/messages', {}, body, {}, function (err, res) { + try { + expect(err).to.equal(null, 'Check that we do not get an error response for a partial success'); + expect(res.success).to.equal(false, 'Check res.success is false for a partial failure'); + expect(res.statusCode).to.equal(400, 'Check HPR.statusCode is 400 for a partial failure'); + expect(res.errorCode).to.equal(40020, 'Check HPR.errorCode is 40020 for a partial failure'); + expect(res.errorMessage, 'Check have an HPR.errorMessage').to.be.ok; + + var response = res.items[0]; + expect(response.error.code).to.equal(40020, 'Verify response has an errorCode'); + expect(response.batchResponse.length).to.equal( + 3, + 'Verify batched response includes response for each channel' + ); + expect(response.batchResponse[0].channel).to.equal(name, 'Verify channel1 response includes correct channel'); + expect(!response.batchResponse[0].error, 'Verify first channel response is not an error').to.be.ok; + expect(response.batchResponse[1].error.code).to.equal( + 40010, + 'Verify [invalid response includes an error with the right code' + ); + expect(response.batchResponse[2].error.code).to.equal( + 40010, + 'Verify empty channel response includes an error with the right code' + ); + done(); + } catch (err) { + done(err); } - test.done(); }); - }; - }) + }); - helper.withMocha('rest/request', exports); -}); \ No newline at end of file + utils.arrForEach(['put', 'patch', 'delete'], function (method) { + it('check' + method, function (done) { + var restEcho = helper.AblyRest({ useBinaryProtocol: false, restHost: echoServerHost, tls: true }); + restEcho.request(method, '/methods', {}, {}, {}, function (err, res) { + if (err) { + done(err); + } else { + try { + expect(res.items[0] && res.items[0].method).to.equal(method); + done(); + } catch (err) { + done(err); + } + } + }); + }); + }); + }); +}); diff --git a/spec/rest/stats.test.js b/spec/rest/stats.test.js index d925075086..a505bbec3b 100644 --- a/spec/rest/stats.test.js +++ b/spec/rest/stats.test.js @@ -1,12 +1,10 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper'], function(Ably, helper) { - var rest, exports = {}, - displayError = helper.displayError, - startTime, intervalStart, timeOffset; +define(['shared_helper', 'chai'], function (helper, chai) { + var rest; + var expect = chai.expect; var lastYear = new Date().getUTCFullYear() - 1; - var anHourAgo = new Date().valueOf() - 60 * 60 * 1000; // Set last interval to 3rd Feb 20xx 16:03:00, JavaScript uses zero based months var firstIntervalEpoch = Date.UTC(lastYear, 1, 3, 15, 3, 0); @@ -14,22 +12,22 @@ define(['ably', 'shared_helper'], function(Ably, helper) { var statsFixtures = [ { intervalId: lastYear + '-02-03:15:03', - inbound: { realtime: { messages: { count: 50, data: 5000 } } }, + inbound: { realtime: { messages: { count: 50, data: 5000 } } }, outbound: { realtime: { messages: { count: 20, data: 2000 } } } }, { intervalId: lastYear + '-02-03:15:04', - inbound: { realtime: { messages: { count: 60, data: 6000 } } }, + inbound: { realtime: { messages: { count: 60, data: 6000 } } }, outbound: { realtime: { messages: { count: 10, data: 1000 } } } }, { intervalId: lastYear + '-02-03:15:05', - inbound: { realtime: { messages: { count: 70, data: 7000 } } }, - outbound: { realtime: { messages: { count: 40, data: 4000 } } }, - persisted: { presence: { count: 20, data: 2000 } }, - connections: { tls: { peak: 20, opened: 10 } }, - channels: { peak: 50, opened: 30 }, - apiRequests: { succeeded: 50, failed: 10 }, + inbound: { realtime: { messages: { count: 70, data: 7000 } } }, + outbound: { realtime: { messages: { count: 40, data: 4000 } } }, + persisted: { presence: { count: 20, data: 2000 } }, + connections: { tls: { peak: 20, opened: 10 } }, + channels: { peak: 50, opened: 30 }, + apiRequests: { succeeded: 50, failed: 10 }, tokenRequests: { succeeded: 60, failed: 20 } } ]; @@ -40,516 +38,566 @@ define(['ably', 'shared_helper'], function(Ably, helper) { var dateId; - for(var i = 0; i < 2; i++) { + for (var i = 0; i < 2; i++) { secondIntervalDate.setMinutes(secondIntervalDate.getMinutes() + 1); - dateId = secondIntervalDate.getFullYear() + "-" + ('0' + (secondIntervalDate.getMonth() + 1)).slice(-2) + "-" + ('0' + secondIntervalDate.getDate()).slice(-2) + ":" + ('0' + secondIntervalDate.getHours()).slice(-2) + ":" + ('0' + secondIntervalDate.getMinutes()).slice(-2); + dateId = + secondIntervalDate.getFullYear() + + '-' + + ('0' + (secondIntervalDate.getMonth() + 1)).slice(-2) + + '-' + + ('0' + secondIntervalDate.getDate()).slice(-2) + + ':' + + ('0' + secondIntervalDate.getHours()).slice(-2) + + ':' + + ('0' + secondIntervalDate.getMinutes()).slice(-2); statsFixtures.push({ intervalId: dateId, - inbound: { realtime: { messages: { count: 15, data: 4000 } } }, + inbound: { realtime: { messages: { count: 15, data: 4000 } } }, outbound: { realtime: { messages: { count: 33, data: 3000 } } } }); } - exports.before = function(test) { - test.expect(1); - // force a new app to be created with first argument true so that stats are not effected by other tests - helper.setupApp(true, function() { - rest = helper.AblyRest(); - helper.createStats(helper.getTestApp(), statsFixtures, function(err) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.ok(true, 'Stats fixtures data created'); - test.done(); + describe('rest/stats', function () { + this.timeout(60 * 1000); + + before(function (done) { + // force a new app to be created with first argument true so that stats are not effected by other tests + helper.setupApp(true, function () { + rest = helper.AblyRest(); + helper.createStats(helper.getTestApp(), statsFixtures, function (err) { + if (err) { + done(err); + return; + } + done(); + }); }); }); - }; - - /** - * Using an interval ID string format, check minute-level inbound and outbound stats match fixture data (forwards) - * @spec : (RSC6b4) - */ - exports.appstats_minute0 = function(test) { - test.expect(1); - rest.stats({ - start: lastYear + '-02-03:15:03', - end: lastYear + '-02-03:15:05', - direction: 'forwards' - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.equal(stats.length, 3, 'Verify 3 stat records found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; - } - test.equal(totalInbound, 50 + 60 + 70, 'Verify all inbound messages found'); - test.equal(totalOutbound, 20 + 10 + 40, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } - }); - }; - - /** - * Using milliseconds since epoch, check minute-level inbound and outbound stats match fixture data (forwards) - * @spec : (RSC6b4) - */ - exports.appstats_minute1 = function(test) { - test.expect(1); - rest.stats({ - start: firstIntervalEpoch, - end: secondIntervalEpoch, - direction: 'forwards' - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.equal(stats.length, 3, 'Verify 3 stat records found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; + /** + * Using an interval ID string format, check minute-level inbound and outbound stats match fixture data (forwards) + * @spec : (RSC6b4) + */ + it('appstats_minute0', function (done) { + rest.stats( + { + start: lastYear + '-02-03:15:03', + end: lastYear + '-02-03:15:05', + direction: 'forwards' + }, + function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length).to.equal(3, 'Verify 3 stat records found'); + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(50 + 60 + 70, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(20 + 10 + 40, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } } - - test.equal(totalInbound, 50 + 60 + 70, 'Verify all inbound messages found'); - test.equal(totalOutbound, 20 + 10 + 40, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } + ); }); - }; - - /** - * Check hour-level inbound and outbound stats match fixture data (forwards) - * @spec : (RSC6b4) - */ - exports.appstats_hour0 = function(test) { - test.expect(1); - rest.stats({ - start: lastYear + '-02-03:15', - end: lastYear + '-02-03:18', - direction: 'forwards', - by: 'hour' - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.equal(stats.length, 1, 'Verify 1 stat record found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; - } - test.equal(totalInbound, 50 + 60 + 70, 'Verify all inbound messages found'); - test.equal(totalOutbound, 20 + 10 + 40, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } - }); - }; - - /** - * Check day-level stats exist (forwards) - * @spec : (RSC6b4) - */ - exports.appstats_day0 = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02-03', - direction: 'forwards', - by: 'day' - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify 1 stat records found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; + /** + * Using milliseconds since epoch, check minute-level inbound and outbound stats match fixture data (forwards) + * @spec : (RSC6b4) + */ + it('appstats_minute1', function (done) { + rest.stats( + { + start: firstIntervalEpoch, + end: secondIntervalEpoch, + direction: 'forwards' + }, + function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length).to.equal(3, 'Verify 3 stat records found'); + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(50 + 60 + 70, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(20 + 10 + 40, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } } - - test.equal(totalInbound, 50 + 60 + 70, 'Verify all inbound messages found'); - test.equal(totalOutbound, 20 + 10 + 40, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } + ); }); - }; - - /** - * Check month-level stats exist (forwards) - * @spec : (RSC6b4) - */ - exports.appstats_month0 = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02', - direction: 'forwards', - by: 'month' - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify 1 stat records found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; - } - test.equal(totalInbound, 50 + 60 + 70, 'Verify all inbound messages found'); - test.equal(totalOutbound, 20 + 10 + 40, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } - }); - }; - - /** - * Check limit query param (backwards) - * @spec : (RSC6b3) - */ - exports.appstats_limit_backwards = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02-03:15:04', - direction: 'backwards', - limit: 1 - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify 1 stat records found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; + /** + * Check hour-level inbound and outbound stats match fixture data (forwards) + * @spec : (RSC6b4) + */ + it('appstats_hour0', function (done) { + rest.stats( + { + start: lastYear + '-02-03:15', + end: lastYear + '-02-03:18', + direction: 'forwards', + by: 'hour' + }, + function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length).to.equal(1, 'Verify 1 stat record found'); + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(50 + 60 + 70, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(20 + 10 + 40, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } } - - test.equal(totalInbound, 60, 'Verify all inbound messages found'); - test.equal(totalOutbound, 10, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } + ); }); - }; - - /** - * Check limit query param (forwards) - * @spec : (RSC6b3) - */ - exports.appstats_limit_forwards = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02-03:15:04', - direction: 'forwards', - limit: 1 - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - try { - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify 1 stat records found'); - - var totalInbound = 0, totalOutbound = 0; - for(var i = 0; i < stats.length; i++) { - totalInbound += stats[i].inbound.all.messages.count; - totalOutbound += stats[i].outbound.all.messages.count; - } - test.equal(totalInbound, 50, 'Verify all inbound messages found'); - test.equal(totalOutbound, 20, 'Verify all outbound messages found'); - test.done(); - } catch(e) { - console.log(e); - } - }); - }; - - /** - * Check query pagination (backwards) - * @spec : (RSC6b2) - */ - exports.appstats_pagination_backwards = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02-03:15:05', - direction: 'backwards', - limit: 1 - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 7000, 'Verify all published message data found'); - - /* get next page */ - test.ok(page.hasNext(), 'Verify next page rel link present'); - page.next(function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - test.expect(6); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 6000, 'Verify all published message data found'); - - /* get next page */ - test.ok(page.hasNext(), 'Verify next page rel link present'); - page.next(function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + /** + * Check day-level stats exist (forwards) + * @spec : (RSC6b4) + */ + it('appstats_day0', function (done) { + rest.stats( + { + end: lastYear + '-02-03', + direction: 'forwards', + by: 'day' + }, + function (err, page) { + if (err) { + done(err); return; } - test.expect(9); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 5000, 'Verify all published message data found'); - - /* verify no further pages */ - test.ok(page.isLast(), 'Verify last page'); - - test.expect(10); + try { + var stats = page.items; + expect(stats.length == 1, 'Verify 1 stat records found').to.be.ok; + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(50 + 60 + 70, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(20 + 10 + 40, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } + } + ); + }); - page.first(function(err, page) { - var totalData = 0; + /** + * Check month-level stats exist (forwards) + * @spec : (RSC6b4) + */ + it('appstats_month0', function (done) { + rest.stats( + { + end: lastYear + '-02', + direction: 'forwards', + by: 'month' + }, + function (err, page) { + if (err) { + done(err); + return; + } + try { var stats = page.items; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 7000, 'Verify all published message data found'); + expect(stats.length == 1, 'Verify 1 stat records found').to.be.ok; + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(50 + 60 + 70, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(20 + 10 + 40, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } + } + ); + }); - /* that's it */ - test.done(); - }); - }); - }); + /** + * Check limit query param (backwards) + * @spec : (RSC6b3) + */ + it('appstats_limit_backwards', function (done) { + rest.stats( + { + end: lastYear + '-02-03:15:04', + direction: 'backwards', + limit: 1 + }, + function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify 1 stat records found').to.be.ok; + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(60, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(10, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } + } + ); }); - }; - - /** - * Check query pagination (forwards) - * @spec : (RSC6b2) - */ - exports.appstats_pagination_forwards = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02-03:15:05', - direction: 'forwards', - limit: 1 - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 5000, 'Verify all published message data found'); - - /* get next page */ - test.ok(page.hasNext(), 'Verify next page rel link present'); - page.next(function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; + + /** + * Check limit query param (forwards) + * @spec : (RSC6b3) + */ + it('appstats_limit_forwards', function (done) { + rest.stats( + { + end: lastYear + '-02-03:15:04', + direction: 'forwards', + limit: 1 + }, + function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify 1 stat records found').to.be.ok; + + var totalInbound = 0, + totalOutbound = 0; + for (var i = 0; i < stats.length; i++) { + totalInbound += stats[i].inbound.all.messages.count; + totalOutbound += stats[i].outbound.all.messages.count; + } + + expect(totalInbound).to.equal(50, 'Verify all inbound messages found'); + expect(totalOutbound).to.equal(20, 'Verify all outbound messages found'); + done(); + } catch (err) { + done(err); + } } - test.expect(6); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 6000, 'Verify all published message data found'); - - /* get next page */ - test.ok(page.hasNext(), 'Verify next page rel link present'); - page.next(function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + ); + }); + + /** + * Check query pagination (backwards) + * @spec : (RSC6b2) + */ + it('appstats_pagination_backwards', function (done) { + rest.stats( + { + end: lastYear + '-02-03:15:05', + direction: 'backwards', + limit: 1 + }, + function (err, page) { + if (err) { + done(err); return; } - test.expect(9); + var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 7000, 'Verify all published message data found'); - - /* verify no further pages */ - test.ok(page.isLast(), 'Verify last page'); + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(7000, 'Verify all published message data found'); + + /* get next page */ + expect(page.hasNext(), 'Verify next page rel link present').to.be.ok; + page.next(function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(6000, 'Verify all published message data found'); + + /* get next page */ + expect(page.hasNext(), 'Verify next page rel link present').to.be.ok; + } catch (err) { + done(err); + return; + } + page.next(function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(5000, 'Verify all published message data found'); + + /* verify no further pages */ + expect(page.isLast(), 'Verify last page').to.be.ok; + } catch (err) { + done(err); + return; + } + + page.first(function (err, page) { + if (err) { + done(err); + return; + } + try { + var totalData = 0; + var stats = page.items; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(7000, 'Verify all published message data found'); + + /* that's it */ + done(); + } catch (err) { + done(err); + return; + } + }); + }); + }); + } + ); + }); - test.expect(10); + /** + * Check query pagination (forwards) + * @spec : (RSC6b2) + */ + it('appstats_pagination_forwards', function (done) { + rest.stats( + { + end: lastYear + '-02-03:15:05', + direction: 'forwards', + limit: 1 + }, + function (err, page) { + if (err) { + done(err); + return; + } - page.first(function(err, page) { - var totalData = 0; + try { var stats = page.items; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 5000, 'Verify all published message data found'); + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(5000, 'Verify all published message data found'); - /* that's it */ - test.done(); + /* get next page */ + expect(page.hasNext(), 'Verify next page rel link present').to.be.ok; + } catch (err) { + done(err); + return; + } + page.next(function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(6000, 'Verify all published message data found'); + + /* get next page */ + expect(page.hasNext(), 'Verify next page rel link present').to.be.ok; + } catch (err) { + done(err); + return; + } + page.next(function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(7000, 'Verify all published message data found'); + + /* verify no further pages */ + expect(page.isLast(), 'Verify last page').to.be.ok; + } catch (err) { + done(err); + return; + } + + page.first(function (err, page) { + if (err) { + done(err); + return; + } + try { + var totalData = 0; + var stats = page.items; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(5000, 'Verify all published message data found'); + + /* that's it */ + done(); + } catch (err) { + done(err); + } + }); + }); }); - }); - }); - }); - }; - - /** - * Check query pagination omitted (defaults to backwards) - * @spec : (RSC6b2) - */ - exports.appstats_pagination_omitted = function(test) { - test.expect(1); - rest.stats({ - end: lastYear + '-02-03:15:05', - limit: 1 - }, function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; - } - - test.expect(3); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 7000, 'Verify all published message data found'); - - /* get next page */ - test.ok(page.hasNext(), 'Verify next page rel link present'); - page.next(function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); - return; } - test.expect(6); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 6000, 'Verify all published message data found'); - - /* get next page */ - test.ok(page.hasNext(), 'Verify next page rel link present'); - page.next(function(err, page) { - if(err) { - test.ok(false, displayError(err)); - test.done(); + ); + }); + + /** + * Check query pagination omitted (defaults to backwards) + * @spec : (RSC6b2) + */ + it('appstats_pagination_omitted', function (done) { + rest.stats( + { + end: lastYear + '-02-03:15:05', + limit: 1 + }, + function (err, page) { + if (err) { + done(err); return; } - test.expect(9); - var stats = page.items; - test.ok(stats.length == 1, 'Verify exactly one stats record found'); - var totalData = 0; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 5000, 'Verify all published message data found'); - /* verify no further pages */ - test.ok(page.isLast(), 'Verify last page'); - - test.expect(10); - - page.first(function(err, page) { - var totalData = 0; + try { var stats = page.items; - for(var i = 0; i < stats.length; i++) - totalData += stats[i].inbound.all.messages.data; - test.equal(totalData, 7000, 'Verify all published message data found'); + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(7000, 'Verify all published message data found'); - /* that's it */ - test.done(); + /* get next page */ + expect(page.hasNext(), 'Verify next page rel link present').to.be.ok; + } catch (err) { + done(err); + return; + } + page.next(function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(6000, 'Verify all published message data found'); + + /* get next page */ + expect(page.hasNext(), 'Verify next page rel link present').to.be.ok; + } catch (err) { + done(err); + return; + } + page.next(function (err, page) { + if (err) { + done(err); + return; + } + try { + var stats = page.items; + expect(stats.length == 1, 'Verify exactly one stats record found').to.be.ok; + var totalData = 0; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(5000, 'Verify all published message data found'); + + /* verify no further pages */ + expect(page.isLast(), 'Verify last page').to.be.ok; + } catch (err) { + done(err); + return; + } + + page.first(function (err, page) { + if (err) { + done(err); + return; + } + try { + var totalData = 0; + var stats = page.items; + for (var i = 0; i < stats.length; i++) totalData += stats[i].inbound.all.messages.data; + expect(totalData).to.equal(7000, 'Verify all published message data found'); + + /* that's it */ + done(); + } catch (err) { + done(err); + } + }); + }); }); - }); - }); + } + ); }); - }; - - helper.withMocha('rest/stats', exports); -}); \ No newline at end of file + }); +}); diff --git a/spec/rest/time.test.js b/spec/rest/time.test.js index 072f16b5dc..647d3006ab 100644 --- a/spec/rest/time.test.js +++ b/spec/rest/time.test.js @@ -1,53 +1,53 @@ -"use strict"; +'use strict'; -define(['ably', 'shared_helper'], function(Ably, helper) { - var rest, exports = {}, - utils = helper.Utils; +define(['shared_helper', 'chai'], function (helper, chai) { + var rest; + var utils = helper.Utils; + var expect = chai.expect; - exports.before = function(test) { - test.expect(1); - helper.setupApp(function(err) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - - rest = helper.AblyRest(); - test.ok(true, 'app set up'); - test.done(); + describe('rest/time', function () { + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + rest = helper.AblyRest(); + done(); + }); }); - }; - exports.time0 = function(test) { - test.expect(1); - rest.time(function(err, serverTime) { - if(err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - var localFiveMinutesAgo = utils.now() - 5 * 60 * 1000; - test.ok(serverTime > localFiveMinutesAgo, 'Verify returned time matches current local time with 5 minute leeway for badly synced local clocks'); - test.done(); + it('time0', function (done) { + rest.time(function (err, serverTime) { + if (err) { + done(err); + return; + } + try { + var localFiveMinutesAgo = utils.now() - 5 * 60 * 1000; + expect( + serverTime > localFiveMinutesAgo, + 'Verify returned time matches current local time with 5 minute leeway for badly synced local clocks' + ).to.be.ok; + done(); + } catch (err) { + done(err); + } + }); }); - }; - exports.timePromise = function(test) { - if(typeof Promise === 'undefined') { - test.done(); - return; + if (typeof Promise !== 'undefined') { + it('timePromise', function (done) { + var rest = helper.AblyRest({ promises: true }); + rest + .time() + .then(function () { + done(); + }) + ['catch'](function (err) { + done(err); + }); + }); } - test.expect(1); - var rest = helper.AblyRest({promises: true}); - rest.time().then(function() { - test.ok(true, 'time succeeded'); - test.done(); - })['catch'](function(err) { - test.ok(false, 'time call failed with error: ' + displayError(err)); - test.done(); - }); - }; - - helper.withMocha('rest/time', exports); -}); \ No newline at end of file + }); +}); diff --git a/spec/support/browser_file_list.js b/spec/support/browser_file_list.js index b36af2fa6d..29efdf2604 100644 --- a/spec/support/browser_file_list.js +++ b/spec/support/browser_file_list.js @@ -1,2 +1,2 @@ window.__karma__ = { base: '../' }; -window.__karma__.files = {"browser/static/ably-commonjs.js":true,"browser/static/ably-commonjs.noencryption.js":true,"browser/static/ably-nativescript.js":true,"browser/static/ably-node.js":true,"browser/static/ably-reactnative.js":true,"browser/static/ably.js":true,"browser/static/ably.min.js":true,"browser/static/ably.noencryption.js":true,"browser/static/ably.noencryption.min.js":true,"browser/lib/util/base64.js":true,"node_modules/async/lib/async.js":true,"node_modules/@ably/vcdiff-decoder/dist/vcdiff-decoder.js":true,"node_modules/crypto-js/build/enc-base64.js":true,"node_modules/crypto-js/build/enc-utf8.js":true,"node_modules/crypto-js/build/core.js":true,"spec/common/globals/environment.js":true,"spec/common/globals/named_dependencies.js":true,"spec/common/modules/client_module.js":true,"spec/common/modules/shared_helper.js":true,"spec/common/modules/testapp_manager.js":true,"spec/common/modules/testapp_module.js":true,"spec/common/ably-common/test-resources/crypto-data-128.json":true,"spec/common/ably-common/test-resources/crypto-data-256.json":true,"spec/common/ably-common/test-resources/messages-encoding.json":true,"spec/common/ably-common/test-resources/presence-messages-encoding.json":true,"spec/common/ably-common/test-resources/test-app-setup.json":true,"spec/support/browser_file_list.js":true,"spec/support/browser_setup.js":true,"spec/support/environment.vars.js":true,"spec/support/modules_helper.js":true,"spec/support/tear_down.js":true,"spec/support/test_helper.js":true,"spec/realtime/auth.test.js":true,"spec/realtime/channel.test.js":true,"spec/realtime/connection.test.js":true,"spec/realtime/connectivity.test.js":true,"spec/realtime/crypto.test.js":true,"spec/realtime/delta.test.js":true,"spec/realtime/encoding.test.js":true,"spec/realtime/event_emitter.test.js":true,"spec/realtime/failure.test.js":true,"spec/realtime/history.test.js":true,"spec/realtime/init.test.js":true,"spec/realtime/message.test.js":true,"spec/realtime/presence.test.js":true,"spec/realtime/reauth.test.js":true,"spec/realtime/resume.test.js":true,"spec/realtime/sync.test.js":true,"spec/realtime/upgrade.test.js":true,"spec/rest/auth.test.js":true,"spec/rest/bufferutils.test.js":true,"spec/rest/capability.test.js":true,"spec/rest/defaults.test.js":true,"spec/rest/fallbacks.test.js":true,"spec/rest/history.test.js":true,"spec/rest/http.test.js":true,"spec/rest/init.test.js":true,"spec/rest/message.test.js":true,"spec/rest/presence.test.js":true,"spec/rest/push.test.js":true,"spec/rest/request.test.js":true,"spec/rest/stats.test.js":true,"spec/rest/time.test.js":true,"spec/browser/connection.test.js":true,"spec/browser/simple.test.js":true}; \ No newline at end of file +window.__karma__.files = {"browser/static/ably-commonjs.js":true,"browser/static/ably-commonjs.noencryption.js":true,"browser/static/ably-nativescript.js":true,"browser/static/ably-node.js":true,"browser/static/ably-reactnative.js":true,"browser/static/ably.js":true,"browser/static/ably.min.js":true,"browser/static/ably.noencryption.js":true,"browser/static/ably.noencryption.min.js":true,"browser/lib/util/base64.js":true,"node_modules/async/lib/async.js":true,"node_modules/@ably/vcdiff-decoder/dist/vcdiff-decoder.js":true,"node_modules/crypto-js/build/enc-base64.js":true,"node_modules/crypto-js/build/enc-utf8.js":true,"node_modules/crypto-js/build/core.js":true,"spec/common/globals/environment.js":true,"spec/common/globals/named_dependencies.js":true,"spec/common/modules/client_module.js":true,"spec/common/modules/shared_helper.js":true,"spec/common/modules/testapp_manager.js":true,"spec/common/modules/testapp_module.js":true,"spec/common/ably-common/test-resources/crypto-data-128.json":true,"spec/common/ably-common/test-resources/crypto-data-256.json":true,"spec/common/ably-common/test-resources/messages-encoding.json":true,"spec/common/ably-common/test-resources/presence-messages-encoding.json":true,"spec/common/ably-common/test-resources/test-app-setup.json":true,"spec/support/browser_file_list.js":true,"spec/support/browser_setup.js":true,"spec/support/environment.vars.js":true,"spec/support/modules_helper.js":true,"spec/support/root_hooks.js":true,"spec/support/test_helper.js":true,"spec/realtime/auth.test.js":true,"spec/realtime/channel.test.js":true,"spec/realtime/connection.test.js":true,"spec/realtime/connectivity.test.js":true,"spec/realtime/crypto.test.js":true,"spec/realtime/delta.test.js":true,"spec/realtime/encoding.test.js":true,"spec/realtime/event_emitter.test.js":true,"spec/realtime/failure.test.js":true,"spec/realtime/history.test.js":true,"spec/realtime/init.test.js":true,"spec/realtime/message.test.js":true,"spec/realtime/presence.test.js":true,"spec/realtime/reauth.test.js":true,"spec/realtime/resume.test.js":true,"spec/realtime/sync.test.js":true,"spec/realtime/upgrade.test.js":true,"spec/rest/auth.test.js":true,"spec/rest/bufferutils.test.js":true,"spec/rest/capability.test.js":true,"spec/rest/defaults.test.js":true,"spec/rest/fallbacks.test.js":true,"spec/rest/history.test.js":true,"spec/rest/http.test.js":true,"spec/rest/init.test.js":true,"spec/rest/message.test.js":true,"spec/rest/presence.test.js":true,"spec/rest/push.test.js":true,"spec/rest/request.test.js":true,"spec/rest/stats.test.js":true,"spec/rest/time.test.js":true,"spec/browser/connection.test.js":true,"spec/browser/simple.test.js":true}; \ No newline at end of file diff --git a/spec/support/root_hooks.js b/spec/support/root_hooks.js new file mode 100644 index 0000000000..3ea55d06aa --- /dev/null +++ b/spec/support/root_hooks.js @@ -0,0 +1,12 @@ +define(['shared_helper'], function (helper) { + after(function (done) { + this.timeout(10 * 1000); + helper.tearDownApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); + }); +}); diff --git a/spec/support/tear_down.js b/spec/support/tear_down.js deleted file mode 100644 index 9901fa50a6..0000000000 --- a/spec/support/tear_down.js +++ /dev/null @@ -1,17 +0,0 @@ -"use strict"; - -define(['shared_helper'], function(helper) { - helper.withMocha('tear_down', { - teardownapp: function(test) { - helper.tearDownApp(function(err) { - if (err) { - test.ok(false, helper.displayError(err)); - test.done(); - return; - } - test.ok(true, 'app torn down'); - test.done(); - }); - } - }); -}); \ No newline at end of file diff --git a/spec/tasks/grunt-mocha.js b/spec/tasks/grunt-mocha.js index b49da586da..2743b40f70 100644 --- a/spec/tasks/grunt-mocha.js +++ b/spec/tasks/grunt-mocha.js @@ -9,8 +9,8 @@ module.exports = function (grunt) { var test = grunt.option('test'), debug = grunt.option('debug'), inspector = grunt.option('inspector'), - helpers = ['spec/support/modules_helper.js', 'spec/support/test_helper.js'], - tearDown = ['spec/support/tear_down.js']; + fgrep = grunt.option('fgrep'), + helpers = ['spec/support/modules_helper.js', 'spec/support/test_helper.js', 'spec/support/root_hooks.js']; function getRelativePath(files) { return files.map(function(helperPath) { @@ -36,11 +36,15 @@ module.exports = function (grunt) { grunt.registerTask('mocha', 'Run the Mocha test suite.\nOptions:\n --test [tests] e.g. --test spec/rest/auth.test.js\n --debug will debug using standard node debugger\n --inspector will start with node inspector', function() { - var runTests = getRelativePath(helpers).concat(['spec/realtime/*.test.js', 'spec/rest/*.test.js']).concat(getRelativePath(tearDown)).join(' '); + var runTests = getRelativePath(helpers).concat(['spec/realtime/*.test.js', 'spec/rest/*.test.js']).join(' '); grunt.log.writeln("Running Mocha test suite against " + (test ? test : 'all tests')); if (test) { - runTests = getRelativePath(helpers).concat(resolveTests(test)).concat(getRelativePath(tearDown)).join(' '); + runTests = getRelativePath(helpers).concat(resolveTests(test)).join(' '); + } + + if (fgrep) { + runTests += ' --fgrep ' + fgrep; } var done = this.async(),