diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..79f8658 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + "parserOptions": { + 'sourceType': 'script', + 'ecmaVersion': 5 + }, + "env": { + "node": true, + "commonjs": true, + "mocha": true, + }, + "globals": { + "Promise": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": ["error", 2], + "linebreak-style": [ "error", "unix"], + "semi": ["error", "always"], + "eol-last": ["error", "always"] + }, + "overrides": [{ + "files": ["src/requestsDebugger.js", "src/commandLine.js", "test/**/*.test.js"], + "rules": { + "no-console": "off" + } + }] +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02b52e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +RequestsDebuggerLogs + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Executables +RequestsDebugger-Mac +RequestsDebugger.exe +RequestsDebugger-Linux diff --git a/README.md b/README.md index 6c8a477..4c7601a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,96 @@ -# requests-debugger -Tool for debugging client side failure of requests, leading to requests getting dropped or not reaching BrowserStack. For internal reference : https://browserstack.atlassian.net/wiki/spaces/ENG/pages/1561081359/Network+Utility+Tool +## Requests Debugger +### Tool for debugging client side failure of requests, leading to requests getting dropped or not reaching BrowserStack. + +## Features +- Proxy Server to intercept requests fired by client bindings to keep track of their flow. +- Connectivity Checker : To check for the reachability of BrowserStack components, i.e. Rails & Hub. +- Multi-Platform Stats Compatibility : Ability to collect stats of CPU, Network & Memory Stats. +- Retry Mechanism in case a request fails at the client side + +## How to run +- Code + - Install all the required packages: `npm install` + - Start Requests Debugger with the required arguments: `npm run start -- `. + - Supported `args`: + - `--port `: Port on which the Requests Debugger Tool's Proxy will run. Default: 9687 + - `--proxy-host `: Hostname of the Upstream Proxy + - `--proxy-port `: Port of the Upstream Proxy. Default: 3128 (if hostname is provided) + - `--proxy-user `: Username for auth of the Upstream Proxy + - `--proxy-pass `: Password for auth of the Upstream Proxy. Default: empty (if username is provided) + - `--retry-delay `: Delay for the retry of a failed request. Default: 1000ms + - `--request-timeout `: Hard timeout for killing the requests being fired from the tool before receiving any response. Default: 260000ms. + - `--logs-path `: Directory where the 'RequestsDebuggerLogs' folder will be created for storing logs. Default: Current Working Directory + - `--del-logs`: Deletes any existing logs from the RequestsDebuggerLogs/ directory and initializes new files for logging + - `--help`: Help for Requests Debugger Tool + - `--version`: Version of the Requests Debugger Tool +- Executable + - Run the Platform Specific executable via terminal/cmd: + - Mac: `./RequestsDebugger-Mac ` + - Linux: `./RequestsDebugger-Linux ` + - Windows: `RequestsDebugger.exe ` + +## How to use +- Since the tool acts like a proxy, you will have to set the proxy to be used by your client binding to `localhost:9687`. i.e. + - For Java: + - ``` + System.getProperties().put("http.proxyHost", "localhost"); + System.getProperties().put("http.proxyPort", "9687"); + ``` + - For Ruby: + - Set your system's env variable `http_proxy=localhost:9687` and Ruby's Selenium Client Binding will pick the value. Or, + - Run you test by giving the environment variable to your command itself, i.e. `http_proxy=localhost:9687 ruby ` + - Similarly, you can also set proxy for other client bindings. + +## Steps to build the executables +- Linux + - `npm run build:linux` +- Mac OS X + - `npm run build:mac` +- Windows + - `npm run build:win` + +## What each log file consists of? +- `Connectivitiy.log` + - It consists the logs of all the connectivity checks which were performed in terms of reachability with BrowserStack componenets, i.e. Hub and Rails. + - The report includes HTTP & HTTPS requests by passing the requests via any upstream proxy (if provided). + - New checks are performed whenever a request fails. +- `CPUStats.log` + - It consists of the load and usage stats of user's CPU while booting up the Requests Debugger Tool. It lists down the top 10 processes which were using the highest CPU at that moment. Note: For Windows, it only includes the `loadPercentage` at that moment. +- `MemStats.log` + - It consists of the memory stats of the user's machine while booting up the tool. It includes: + - Total Memory + - Free Memory + - Used Memory + - Swap Total + - Swap Free + - Swap Used +- `NetworkStats.log` + - It consists the list of connections/sockets being established/listening via the user's machine. + - It also includes the report of ping checks with Hub & Rails. + - New stats are appended whenever a request fails. +- `Requests.log` + - It includes the logs of requests being passed via the Requests Debugger Tool. + - Example Logs: + - ``` + TIMESTAMP_IN_UTC [#2::UNIQUE_IDENTIFIER] [Request Start] [INFO] POST http:///wd/hub/status, {"headers":{"......."}} + TIMESTAMP_IN_UTC [#2::UNIQUE_IDENTIFIER] [Request End] [INFO] POST http:///wd/hub/status, {"data":"{\"key\": \"value\"}"} + TIMESTAMP_IN_UTC [#2::UNIQUE_IDENTIFIER] [Tool Request - Retries Left: 1] [INFO] POST http:///wd/hub/status, {"method":"POST","headers":{".......","Proxy-Authorization":"Basic AaBbCcDdEeFfGg==","X-Requests-Debugger":"2::UNIQUE_IDENTIFIER"},"host":"","port":"3130","path":"http:///wd/hub/status","data":"{\"key\": \"value\"}"} + TIMESTAMP_IN_UTC [#2::UNIQUE_IDENTIFIER] [Tool Request - Retries Left: 1] [ERROR] POST http:///wd/hub/status, {"errorMessage":"Error: getaddrinfo ENOTFOUND :3130"} + TIMESTAMP_IN_UTC [#2::UNIQUE_IDENTIFIER] [Tool Request - Retries Left: 0] [ERROR] POST http:///wd/hub/status, {"errorMessage":"Error: getaddrinfo ENOTFOUND :3130"} + TIMESTAMP_IN_UTC [#2::UNIQUE_IDENTIFIER] [Response End] [ERROR] POST http:///wd/hub/status, Status Code: 500, {"message":"Error: getaddrinfo ENOTFOUND :3130. Request Failed At Requests Debugger","error":"Request Failed At Requests Debugger"} + - Tags & their meaning: + - **TIMESTAMP_IN_UTC** : Timestamp of each event in UTC. + - **#2** : This represents the `nth` request which the tool served from the time it was started. + - **UNIQUE_IDENTIFIER** : `uuid` for the request. This is used in `X-Requests-Debugger` custom header for usage & debugging at BrowserStack's end. + - **Request Start** : Client request entering the Requests Debugger Tool. + - **Request End** : This is logged when the client request is finished, i.e. in case of `POST` request, when the client request has pushed all the data. + - **Tool Request - Retries Left: X** : Request which was fired from the tool for the respective client request. It also mentions the retries left for the request. Max retries = 1. A request is retried in case it fails at the tool itself, i.e `ENOTFOUND` etc. + - **Response End** : This specifies the response which was finally sent to the client. +- `RDT_Error.log` + - This is to log any unexpected errors which might occur while using the tool. + + +## Note +- The tool is written in a manner to make it compatible with Node 4.4.0. +- `npm test` sets up a server which runs on port 8787. +- Building the executables require Node >=4.9.1. diff --git a/config/constants.js b/config/constants.js new file mode 100644 index 0000000..16fd129 --- /dev/null +++ b/config/constants.js @@ -0,0 +1,95 @@ +module.exports.VERSION = '1.0.0'; +module.exports.HUB_STATUS_URL = 'http://hub-cloud.browserstack.com/wd/hub/status'; +module.exports.RAILS_AUTOMATE = 'http://automate.browserstack.com'; +module.exports.CONNECTIVITY_REQ_TIMEOUT = 30000; +module.exports.DEFAULT_PROXY_PORT = 3128; +module.exports.MAX_RETRIES = 1; +module.exports.LOGS_FOLDER = 'RequestsDebuggerLogs'; +module.exports.PORTS = { + MAX: 65535, + MIN: 1 +}; + +module.exports.LOGS = Object.freeze({ + NETWORK: 'NetworkStats.log', + CPU: 'CPUStats.log', + MEM: 'MemStats.log', + REQUESTS: 'Requests.log', + CONNECTIVITY: 'Connectivity.log', + ERROR: 'RDT_Error.log' +}); + +module.exports.RdGlobalConfig = { + RETRY_DELAY: 1000, // in ms + RD_HANDLER_PORT: process.env.NODE_ENV === 'test' ? 8787 : 9687, + CLIENT_REQ_TIMEOUT: 260000, // in ms +}; + +module.exports.COMMON = Object.freeze({ + PING_HUB: 'ping -c 5 hub-cloud.browserstack.com', + PING_AUTOMATE: 'ping -c 5 automate.browserstack.com' +}); + +module.exports.MAC = Object.freeze({ + TCP_LISTEN_ESTABLISHED: 'lsof -PiTCP', + TOP_3_SAMPLES: 'top -n 10 -l 3 -stats pid,command,cpu,cpu_others,time,threads,ports,mem,vsize,pgrp,ppid,cycles', + SWAP_USAGE: 'sysctl -n vm.swapusage' +}); + +module.exports.LINUX = Object.freeze({ + TCP_LISTEN_ESTABLISHED: 'lsof -PiTCP', + TOP_3_SAMPLES: 'top -bn 3', + PROC_MEMINFO: '/proc/meminfo' +}); + +module.exports.WIN = Object.freeze({ + NETSTAT_TCP: 'netstat -anosp tcp', + NETSTAT_ROUTING_TABLE: 'netstat -r', + IPCONFIG_ALL: 'ipconfig /all', + SWAP_USAGE: 'pagefile get AllocatedBaseSize, CurrentUsage', // this is a WMIC command. Prefix with WMIC Path + PING_HUB: 'ping -n 5 hub-cloud.browserstack.com', + PING_AUTOMATE: 'ping -n 5 automate.browserstack.com', + LOAD_PERCENTAGE: 'cpu get loadpercentage', // prefix wmic path +}); + +module.exports.ERROR_CODES = Object.freeze({ + EEXIST: 'EEXIST' +}); + +module.exports.TOPICS = Object.freeze({ + LINUX_MEM: 'Linux-Mem', + MAC_MEM: 'Mac-Mem', + WIN_MEM: 'Win-Mem', + TOOL_REQUEST_WITH_RETRIES: 'Tool Request - Retries Left: ', + TOOL_RESPONSE_ERROR: 'Tool Response', + CLIENT_REQUEST_WITH_RETRIES: 'Request - Retries Left: ', + CLIENT_REQUEST_END: 'Request End', + CLIENT_REQUEST_START: 'Request Start', + CLIENT_RESPONSE_END: 'Response End', + NO_TOPIC: 'NO_TOPIC', + UNEXPECTED_ERROR: 'UNEXPECTED_ERROR' +}); + +module.exports.STATIC_MESSAGES = Object.freeze({ + STARTING_TOOL: 'Starting Requests Debugger Tool', + CHECK_CPU_STATS: 'Stats : Checking CPU Stats', + CHECK_NETWORK_STATS: 'Stats : Checking Network Stats', + CHECK_MEMORY_STATS: 'Stats : Checking Memory Stats', + CHECK_CONNECTIVITY: 'Checks : Checking Connectivity With BrowserStack', + ERR_STARTING_TOOL: 'Error in starting Requests Debugger Tool Proxy: ', + TOOL_STARTED_ON_PORT: 'Requests Debugger Tool Proxy Started on Port: ', + CPU_STATS_COLLECTED: 'Stats : Initial CPU Stats Collected', + NETWORK_STATS_COLLECTED: 'Stats : Initial Network Stats Collected', + MEMORY_STATS_COLLECTED: 'Stats : Initial Memory Stats Collected', + CONNECTIVITY_CHECKS_DONE: 'Checks : Connectivity Checks Performed with BrowserStack', + NO_REPORT_GENERATED: 'COULD NOT GENERATE REPORT FOR : ', + REQ_TIMED_OUT: 'Request Timed Out. Did not get any response for ', + REQ_FAILED_MSG: 'Request Failed At Requests Debugger', + BASE_STATS_DESC: 'Base Object for System & Network Stats', + CPU_STATS_NOT_IMPLEMENTED: 'CPU Stats Not Yet Implemented', + MEM_STATS_NOT_IMPLEMENTED: 'Mem Stats Not Yet Implemented', + NETWORK_STATS_NOT_IMPLEMENTED: 'Network Stats Not Yet Implemented', + LINUX_STATS_DESC: 'System and Network Stats for Linux', + MAC_STATS_DESC: 'System and Network Stats for Mac', + WIN_STATS_DESC: 'System and Network Stats for Windows' +}); diff --git a/hooks/pre-push b/hooks/pre-push new file mode 100755 index 0000000..ace3828 --- /dev/null +++ b/hooks/pre-push @@ -0,0 +1,10 @@ +#!/bin/sh + +# copy the hook again +npm run copyhooks + +# run the npm tests +npm test + +# return the 'npm test' exit code +exit $? diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9a49538 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1445 @@ +{ + "name": "requestsdebugger", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", + "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.1" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", + "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", + "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.1", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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" + } + }, + "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, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "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, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "acorn": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "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 + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "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 + }, + "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 + }, + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "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, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha1-dgqnLPION5XoSxKHfODoNzeqKeU=", + "dev": true, + "requires": { + "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" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "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, + "requires": { + "color-name": "~1.1.4" + } + }, + "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 + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "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 + }, + "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, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "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, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "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 + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "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 + }, + "eslint": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.2.0.tgz", + "integrity": "sha512-B3BtEyaDKC5MlfDa2Ha8/D6DsS4fju95zs0hjS3HdGazw+LNayai38A25qMppK37wWGWNYSPOR6oYzlz5MHsRQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.0.0", + "eslint-visitor-keys": "^1.2.0", + "espree": "^7.1.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", + "inquirer": "^7.0.0", + "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.14", + "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" + }, + "dependencies": { + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "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 + }, + "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, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "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" + } + }, + "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 + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "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, + "requires": { + "prelude-ls": "^1.2.1" + } + } + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz", + "integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==", + "dev": true + }, + "espree": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz", + "integrity": "sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==", + "dev": true, + "requires": { + "acorn": "^7.2.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.2.0" + } + }, + "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 + }, + "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, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "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==", + "dev": true + }, + "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 + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "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, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "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, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "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 + }, + "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 + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "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" + } + }, + "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, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "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 + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "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, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "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 + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "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, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "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 + }, + "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, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "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==", + "dev": true + }, + "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 + }, + "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=", + "dev": true + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "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 + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "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, + "requires": { + "minimist": "^1.2.5" + } + }, + "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 + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "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 + }, + "nock": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz", + "integrity": "sha1-2W4Jm+m8HQGJp39EkLu7Jlw4G0k=", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^3.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=", + "dev": true + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "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 + }, + "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, + "requires": { + "callsites": "^3.0.0" + } + }, + "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 + }, + "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 + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "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 + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "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, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "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 + }, + "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 + }, + "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, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "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 + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "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, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "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, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "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 + }, + "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 + }, + "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 + }, + "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, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "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, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "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 + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "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 + }, + "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 + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "winston": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", + "integrity": "sha1-oB5NHQoQPPTq2m/B+IazEQ1xw0s=", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + } + }, + "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 + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1128514 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "requestsdebugger", + "version": "1.0.0", + "description": "Tool to debug failed/dropped requests at client side", + "main": "src/requestsDebugger.js", + "scripts": { + "start": "NODE_ENV=prod node src/requestsDebugger.js", + "lint": "./node_modules/.bin/eslint 'src/*' 'test/*' 'config/*.js'", + "test": "npm run lint; NODE_ENV=test nyc --reporter=html ./node_modules/mocha/bin/mocha 'test/**/*.test.js'", + "build:mac": "npm install; ./node_modules/pkg/lib-es5/bin.js -t node4-macos src/requestsDebugger.js; mv requestsDebugger RequestsDebugger-Mac", + "build:linux": "npm install; ./node_modules/pkg/lib-es5/bin.js -t node4-linux src/requestsDebugger.js; mv requestsDebugger RequestsDebugger-Linux", + "build:win": "npm install; ./node_modules/pkg/lib-es5/bin.js -t node4-win src/requestsDebugger.js; mv requestsDebugger.exe RequestsDebugger.exe", + "copyhooks": "cp hooks/* .git/hooks/; chmod +x .git/hooks/*", + "postinstall": "npm run copyhooks" + }, + "bin": { + "RequestsDebugger": "src/requestsDebugger.js" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "chai": "4.2.0", + "eslint": "4.19.1", + "mocha": "5.2.0", + "nock": "9.6.1", + "nyc": "11.9.0", + "pkg": "4.3.1", + "sinon": "7.5.0" + }, + "dependencies": { + "uuid": "8.2.0", + "winston": "2.4.4" + }, + "nyc": { + "all": true, + "exclude": [ + "**/*.test.js", + "test/testHelper.js", + "node_modules", + "coverage", + "RequestsDebuggerLogs", + "config" + ] + } +} diff --git a/src/commandLine.js b/src/commandLine.js new file mode 100644 index 0000000..9b494cb --- /dev/null +++ b/src/commandLine.js @@ -0,0 +1,249 @@ +/** + * Command Line Manager to parse the command line arguments + * and set the necessary fields in RdGlobalConfig. + */ + +var constants = require('../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; + +var CommandLineManager = { + helpForArgs: function () { + var helpOutput = "\nRequests Debugger - A Proxy Tool for debugging request failures leading to dropping of requests\n" + + "or not being able to reach BrowserStack.\n" + + "\n" + + "Usage: RequestsDebugger [ARGUMENTS]\n\n" + + "ARGUMENTS:\n" + + " --port : Port on which the Requests Debugger Tool's Proxy will run\n" + + " Default: " + RdGlobalConfig.RD_HANDLER_PORT + "\n" + + " --proxy-host : Hostname of the Upstream Proxy\n" + + " --proxy-port : Port of the Upstream Proxy. Default: " + constants.DEFAULT_PROXY_PORT + " (if hostname is provided)\n" + + " --proxy-user : Username for auth of the Upstream Proxy\n" + + " --proxy-pass : Password for auth of the Upstream Proxy\n" + + " --retry-delay : Delay for the retry of a failed request. Default: " + RdGlobalConfig.RETRY_DELAY + "ms\n" + + " --request-timeout : Hard timeout for the requests being fired by the tool before receiving any response\n" + + " Default: " + RdGlobalConfig.CLIENT_REQ_TIMEOUT + "ms\n" + + " --logs-path : Directory where the '" + constants.LOGS_FOLDER + "' folder will be created\n" + + " for storing logs. Default: Current Working Directory\n" + + " --del-logs : Deletes any existing logs from the " + constants.LOGS_FOLDER + "/ directory and initializes\n" + + " new files for logging\n" + + " --help : Help for Requests Debugger Tool\n" + + " --version : Version of the Requests Debugger Tool\n"; + + console.log(helpOutput); + }, + + validArgValue: function (value) { + return value && value.length > 0 && !value.startsWith('--'); + }, + + /** + * Processes the args from the given input array and sets the global config. + * @param {Array} argv + */ + processArgs: function (argv) { + + /* eslint-disable no-undef */ + var missingArgs = new Set(); + var invalidArgs = new Set(); + /* eslint-enable no-undef */ + + // help argument. Logs the CLI usage and exits + var index = argv.indexOf('--help'); + if (index !== -1) { + CommandLineManager.helpForArgs(); + process.exit(0); + } + + // version argument. Logs the version and exits + index = argv.indexOf('--version'); + if (index !== -1) { + console.log("Version:", constants.VERSION); + process.exit(0); + } + + // port for Requests Debugger Proxy + index = argv.indexOf('--port'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + var probablePort = parseInt(argv[index + 1]); + if (!isNaN(probablePort) && (probablePort <= constants.PORTS.MAX) && (probablePort >= constants.PORTS.MIN)) { + RdGlobalConfig.RD_HANDLER_PORT = probablePort; + } else { + console.log("\nPort can only range from:", constants.PORTS.MIN, "to:", constants.PORTS.MAX); + invalidArgs.add('--port'); + } + argv.splice(index, 2); + } else { + invalidArgs.add('--port'); + argv.splice(index, 1); + } + } + + // delay for retries in case of request failures + index = argv.indexOf('--retry-delay'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + var probableDelay = parseFloat(argv[index + 1]); + if (!isNaN(probableDelay) && probableDelay >= 0) { + RdGlobalConfig.RETRY_DELAY = probableDelay; + } else { + console.log("\nDelay for retries should be a valid number"); + invalidArgs.add('--retry-delay'); + } + argv.splice(index, 2); + } else { + invalidArgs.add('--retry-delay'); + argv.splice(index, 1); + } + } + + // timeout for requests being fired from the tool + index = argv.indexOf('--request-timeout'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + var clientRequestDelay = parseFloat(argv[index + 1]); + if (!isNaN(clientRequestDelay) && clientRequestDelay >= 0) { + RdGlobalConfig.CLIENT_REQ_TIMEOUT = clientRequestDelay; + } else { + console.log("\nTimeout for requests should be a valid number"); + invalidArgs.add('--request-timeout'); + } + argv.splice(index, 2); + } else { + invalidArgs.add('--request-timeout'); + argv.splice(index, 1); + } + } + + // process proxy host + index = argv.indexOf('--proxy-host'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + RdGlobalConfig.proxy = RdGlobalConfig.proxy || {}; + RdGlobalConfig.proxy.host = argv[index + 1]; + argv.splice(index, 2); + } else { + invalidArgs.add('--proxy-host'); + argv.splice(index, 1); + } + } + + // process proxy port + index = argv.indexOf('--proxy-port'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + if (RdGlobalConfig.proxy && RdGlobalConfig.proxy.host) { + var probableProxyPort = parseInt(argv[index + 1]); + if (!isNaN(probableProxyPort) && (probableProxyPort <= constants.PORTS.MAX) && (probableProxyPort >= constants.PORTS.MIN)) { + RdGlobalConfig.proxy.port = probableProxyPort; + } else { + console.log("\nProxy Port can only range from:", constants.PORTS.MIN, "to:", constants.PORTS.MAX); + invalidArgs.add('--proxy-port'); + } + } else { + if (!invalidArgs.has('--proxy-host')) missingArgs.add('--proxy-host'); + } + argv.splice(index, 2); + } else { + invalidArgs.add('--proxy-port'); + argv.splice(index, 1); + } + } + + // if proxy port value in invalid or doesn't exist and host exists, set the default value + if (RdGlobalConfig.proxy && RdGlobalConfig.proxy.host && (invalidArgs.has('--proxy-port') || !RdGlobalConfig.proxy.port)) { + console.log('\nSetting Default Proxy Port:', constants.DEFAULT_PROXY_PORT, '\n'); + RdGlobalConfig.proxy.port = constants.DEFAULT_PROXY_PORT; + invalidArgs.delete('--proxy-port'); + } + + // process proxy user + index = argv.indexOf('--proxy-user'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + RdGlobalConfig.proxy = RdGlobalConfig.proxy || {}; + RdGlobalConfig.proxy.username = argv[index + 1]; + if (!(RdGlobalConfig.proxy && RdGlobalConfig.proxy.host)) { + if (!invalidArgs.has('--proxy-host')) missingArgs.add('--proxy-host'); + } + argv.splice(index, 2); + } else { + invalidArgs.add('--proxy-user'); + argv.splice(index, 1); + } + } + + // process proxy pass + index = argv.indexOf('--proxy-pass'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + if (RdGlobalConfig.proxy && RdGlobalConfig.proxy.username) { + RdGlobalConfig.proxy.password = argv[index + 1]; + } else { + if (!invalidArgs.has('--proxy-user')) missingArgs.add('--proxy-user'); + } + argv.splice(index, 2); + } else { + invalidArgs.add('--proxy-pass'); + argv.splice(index, 1); + } + } + + // if proxy pass is invalid or doesn't exist and username exists, set the password as empty + if (RdGlobalConfig.proxy && RdGlobalConfig.proxy.username && (invalidArgs.has('--proxy-pass') || !RdGlobalConfig.proxy.password)) { + console.log('Setting Proxy Password as Empty\n'); + RdGlobalConfig.proxy.password = ''; + invalidArgs.delete('--proxy-pass'); + } + + // process arguments which decides whether existing logs should be deleted or appended + index = argv.indexOf('--del-logs'); + if (index !== -1) { + RdGlobalConfig.DELETE_EXISTING_LOGS = true; + argv.splice(index, 1); + } else { + RdGlobalConfig.DELETE_EXISTING_LOGS = false; + } + + index = argv.indexOf('--logs-path'); + if (index !== -1) { + if (CommandLineManager.validArgValue(argv[index + 1])) { + RdGlobalConfig.logsPath = argv[index + 1]; + argv.splice(index, 2); + } else { + invalidArgs.add('--logs-path'); + argv.splice(index, 1); + } + } + + var exitAfterProcessArgs = false; + missingArgs = Array.from(missingArgs); + + // check for any more invalid args which were not processed above + if (argv.length > 2) { + argv.slice(2).forEach(function (invalidArg) { + invalidArgs.add(invalidArg); + }); + } + + invalidArgs = Array.from(invalidArgs); + + if (invalidArgs.length) { + console.log('\nInvalid Argument(s): ', invalidArgs.join(', '), '\n'); + exitAfterProcessArgs = true; + } + + if (missingArgs.length) { + console.log('\nMissing Argument(s): ', missingArgs.join(', '), '\n'); + exitAfterProcessArgs = true; + } + + // exit the tool by logging the args helper + if (exitAfterProcessArgs) { + CommandLineManager.helpForArgs(); + process.exit(0); + } + } +}; + +module.exports = CommandLineManager; diff --git a/src/connectivity.js b/src/connectivity.js new file mode 100644 index 0000000..e908d58 --- /dev/null +++ b/src/connectivity.js @@ -0,0 +1,185 @@ +/** + * Connectivity Checker to perform basic connectivity checks with BrowserStack + * components, i.e. Hub and Rails. + * It performs checks with/without proxy, over HTTP(S). + * NOTE : HTTPS with Proxy is not implemented since the tool is anyway capturing HTTP + * traffic only. Shall be taken up later. + */ + +var http = require('http'); +var url = require('url'); +var constants = require('../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; +var Utils = require('./utils'); +var https = require('https'); + +/** + * Fires the requests to perform connectivity checks + * @param {Object} requestOptions + * @param {'http'|'https'} requestType + * @param {String} description + * @param {Array} successCodes + * @param {Function} callback + */ +var fireRequest = function (requestOptions, requestType, description, successCodes, callback) { + var httpOrHttps = requestType === 'http' ? http : https; + var responseData = { + data: [], + statusCode: null, + errorMessage: null, + description: description, + result: 'Failed' + }; + + var request = httpOrHttps.request(requestOptions, function (response) { + responseData.statusCode = response.statusCode; + if (successCodes.indexOf(response.statusCode) !== -1) { + responseData.result = "Passed"; + } + + response.on('data', function (chunk) { + responseData.data.push(chunk); + }); + + response.on('end', function () { + responseData.data = Buffer.concat(responseData.data).toString(); + callback(responseData); + }); + + }); + + request.on('error', function (err) { + responseData.errorMessage = err.toString(); + callback(responseData); + }); + + request.setTimeout(constants.CONNECTIVITY_REQ_TIMEOUT, function () { + request.destroy("Request Timed Out"); + }); + + request.end(); +}; + + +var ConnectivityChecker = { + + connectionChecks: [], + + reqOpsWithoutProxy: function () {}, + reqOpsWithProxy: function () {}, + + httpToHubWithoutProxy: function (callback) { + var requestUrl = constants.HUB_STATUS_URL; + var requestOptions = ConnectivityChecker.reqOpsWithoutProxy(requestUrl, 'http'); + fireRequest(requestOptions, 'http', 'HTTP Request To Hub Without Proxy', [200], function (response) { + callback(response); + }); + }, + + httpToRailsWithoutProxy: function (callback) { + var requestUrl = constants.RAILS_AUTOMATE; + var requestOptions = ConnectivityChecker.reqOpsWithoutProxy(requestUrl, 'http'); + fireRequest(requestOptions, 'http', 'HTTP Request To Rails Without Proxy', [200, 301], function (response) { + callback(response); + }); + }, + + httpsToHubWithoutProxy: function (callback) { + var requestUrl = constants.HUB_STATUS_URL; + var requestOptions = ConnectivityChecker.reqOpsWithoutProxy(requestUrl, 'https'); + fireRequest(requestOptions, 'https', 'HTTPS Request To Hub Without Proxy', [200], function (response) { + callback(response); + }); + }, + + httpsToRailsWithoutProxy: function (callback) { + var requestUrl = constants.RAILS_AUTOMATE; + var requestOptions = ConnectivityChecker.reqOpsWithoutProxy(requestUrl, 'https'); + fireRequest(requestOptions, 'https', 'HTTPS Request to Rails Without Proxy', [301, 302], function (response) { + callback(response); + }); + }, + + httpToHubWithProxy: function (callback) { + var requestUrl = constants.HUB_STATUS_URL; + var requestOptions = ConnectivityChecker.reqOpsWithProxy(requestUrl, 'http'); + fireRequest(requestOptions, 'http', 'HTTP Request To Hub With Proxy', [200], function (response) { + callback(response); + }); + }, + + httpToRailsWithProxy: function (callback) { + var requestUrl = constants.RAILS_AUTOMATE; + var requestOptions = ConnectivityChecker.reqOpsWithProxy(requestUrl, 'http'); + fireRequest(requestOptions, 'http', 'HTTP Request To Rails With Proxy', [301], function (response) { + callback(response); + }); + }, + + + /** + * Decides the checks to perform based on whether any proxy is provided by the + * user or not.Along with the checks, it builds the request options template + * required to fire the connectivity check requests. + */ + decideConnectionChecks: function () { + if (!ConnectivityChecker.connectionChecks.length) { + ConnectivityChecker.connectionChecks = [this.httpToHubWithoutProxy, this.httpToRailsWithoutProxy, this.httpsToHubWithoutProxy, this.httpsToRailsWithoutProxy]; + ConnectivityChecker.reqOpsWithoutProxy = function (reqUrl, reqType) { + var parsedUrl = url.parse(reqUrl); + var reqOptions = { + method: 'GET', + headers: {}, + host: parsedUrl.hostname, + port: parsedUrl.port || ( reqType === 'http' ? 80 : 443 ), + path: parsedUrl.path + }; + return reqOptions; + }; + + if (RdGlobalConfig.proxy) { + ConnectivityChecker.connectionChecks.push(this.httpToHubWithProxy, this.httpToRailsWithProxy); + /* eslint-disable-next-line no-unused-vars */ + ConnectivityChecker.reqOpsWithProxy = function (reqUrl, reqType) { + var parsedUrl = url.parse(reqUrl); + var reqOptions = { + method: 'GET', + headers: {}, + host: RdGlobalConfig.proxy.host, + port: RdGlobalConfig.proxy.port, + path: parsedUrl.href + }; + if (RdGlobalConfig.proxy.username && RdGlobalConfig.proxy.password) { + reqOptions.headers['Proxy-Authorization'] = Utils.proxyAuthToBase64(RdGlobalConfig.proxy); + } + return reqOptions; + }; + } + } + }, + + /** + * Fires the Connectivity Checks in Async Manner + * @param {String} topic + * @param {Number|String} uuid + * @param {Function} callback + */ + fireChecks: function (topic, uuid, callback) { + ConnectivityChecker.decideConnectionChecks(); + var totalChecksDone = 0; + var checksResult = new Array(ConnectivityChecker.connectionChecks.length); + ConnectivityChecker.connectionChecks.forEach(function (check, index) { + check(function (checkData) { + checksResult[index] = checkData; + + if (++totalChecksDone === ConnectivityChecker.connectionChecks.length) { + checksResult = Utils.beautifyObject(checksResult, "Result Key", "Result Value"); + RdGlobalConfig.connLogger.info(topic, checksResult, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + } + }); + }); + } +}; + +module.exports = ConnectivityChecker; diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..6bbdc43 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,57 @@ +/** + * Base Logger to help initialize loggers for different purposes. + */ + +var winston = require('winston'); + +var LogManager = { + getLogger: function (filename) { + return new winston.Logger({ + transports: [ + new winston.transports.File({ + filename: filename + }) + ] + }); + }, + + /** + * Initializes a logger to the given file and returns the methods to + * interact with the logger. + * Currently, 'info' and 'error' are defined. THe rest can be taken up later if required. + * @param {String} filename + */ + initializeLogger: function (filename) { + var newLogger = LogManager.getLogger(filename); + newLogger.transports.file.timestamp = function () { + return (new Date().toISOString()); + }; + newLogger.transports.file.formatter = function (options) { + return options.timestamp() + + (options.meta.uuid ? ' [#' + options.meta.uuid + ']': '' ) + + (options.meta.topic ? ' [' + options.meta.topic + ']' : '' ) + + ' [' + options.level.toUpperCase() + ']' + + ' ' + options.message; + }; + newLogger.transports.file.json = false; + + newLogger.info("************* LOGGER INITIALIZED **************\r\n"); + + return { + info: function (topic, message, stringify, data, uuid) { + stringify = stringify || false; + message = stringify ? JSON.stringify(message) : message; + data = JSON.stringify(data); + newLogger.info(message + ', ' + (data !== '{}' ? data : ""), { topic: topic || '', uuid: uuid || '' }); + }, + error: function (topic, message, stringify, data, uuid) { + stringify = stringify || false; + message = stringify ? JSON.stringify(message) : message; + data = JSON.stringify(data); + newLogger.error(message + ', ' + (data !== '{}' ? data : ""), { topic: topic || '', uuid: uuid || '' }); + } + }; + } +}; + +module.exports = LogManager; diff --git a/src/requestLib.js b/src/requestLib.js new file mode 100644 index 0000000..4e5e318 --- /dev/null +++ b/src/requestLib.js @@ -0,0 +1,146 @@ +var http = require('http'); +var constants = require('../config/constants'); +var Utils = require('./utils'); +var keepAliveAgent = new http.Agent({ + keepAlive: true +}); +var RdGlobalConfig = constants.RdGlobalConfig; + +var RequestLib = { + /** + * Method to perform the request on behalf of the client + * @param {{request: Object, furtherRequestOptions: Object}} params + * @param {http.IncomingMessage} clientRequest + * @param {Number} retries + */ + _makeRequest: function (params, clientRequest, retries) { + return new Promise(function (resolve, reject) { + var requestOptions = Object.assign({}, params.furtherRequestOptions, { + agent: keepAliveAgent + }); + + // Adding a custom header for usage and debugging purpose at BrowserStack + requestOptions.headers['X-Requests-Debugger'] = clientRequest.id; + + // Initialize the request to be fired on behalf of the client + var request = http.request(requestOptions, function (response) { + var responseToSend = { + statusCode: response.statusCode, + headers: response.headers, + data: [] + }; + + response.on('data', function (chunk) { + responseToSend.data.push(chunk); + }); + + response.on('end', function () { + responseToSend.data = Buffer.concat(responseToSend.data).toString(); + resolve(responseToSend); + }); + + response.on('error', function (err) { + reject({ + message: err, + customTopic: constants.TOPICS.TOOL_RESPONSE_ERROR + }); + }); + }); + + // Log the request that will be initiated on behalf of the client + request.on('finish', function () { + RdGlobalConfig.reqLogger.info(constants.TOPICS.TOOL_REQUEST_WITH_RETRIES + retries, clientRequest.method + ' ' + clientRequest.url, + false, + Object.assign({}, params.furtherRequestOptions, { + data: Buffer.concat(params.request.data).toString() + }), + clientRequest.id); + }); + + // Capture any error scenarios while making the request on behalf of the client + request.on('error', function (err) { + reject({ + message: err, + customTopic: constants.TOPICS.TOOL_REQUEST_WITH_RETRIES + retries + }); + }); + + // Set a hard timeout for the request being initiated. + request.setTimeout(RdGlobalConfig.CLIENT_REQ_TIMEOUT, function () { + request.destroy(constants.STATIC_MESSAGES.REQ_TIMED_OUT + RdGlobalConfig.CLIENT_REQ_TIMEOUT + ' ms'); + }); + + /** + * If its the first try of the request, set up all the event listeners to capture/collect + * the data being sent by the client. + * If its not the first try, then we already have the data to recreate the request + * if the previous try fails. + */ + if (retries === constants.MAX_RETRIES) { + clientRequest.on('data', function (chunk) { + params.request.data.push(chunk); + if (!request.write(chunk)) { + clientRequest.pause(); + request.once('drain', function () { + clientRequest.resume(); + }); + } + }); + + clientRequest.on('error', function (err) { + request.end(); + reject({ + message: err, + customTopic: constants.TOPICS.CLIENT_REQUEST_WITH_RETRIES + retries + }); + }); + + clientRequest.on('end', function () { + RdGlobalConfig.reqLogger.info(constants.TOPICS.CLIENT_REQUEST_END, params.request.method + ' ' + params.request.url, false, { + data: Buffer.concat(params.request.data).toString() + }, + clientRequest.id); + request.end(); + }); + } else { + request.write(Buffer.concat(params.request.data)); + request.end(); + } + }); + }, + + /** + * Handler for performing request. Includes the retry mechanism when request fails. + * @param {{request: Object, furtherRequestOptions: Object}} params + * @param {http.IncomingMessage} clientRequest + * @param {Number} retries + */ + call: function (params, clientRequest, retries) { + retries = (typeof retries === 'number') ? Math.min(constants.MAX_RETRIES, Math.max(retries, 0)) : constants.MAX_RETRIES; + return RequestLib._makeRequest(params, clientRequest, retries) + .catch(function (err) { + var errTopic = err.customTopic || constants.TOPICS.UNEXPECTED_ERROR; + // Collect Network & Connectivity Logs whenever a request fails + RdGlobalConfig.networkLogHandler(errTopic, clientRequest.id); + RdGlobalConfig.connHandler(errTopic, clientRequest.id); + + if (retries > 0) { + RdGlobalConfig.reqLogger.error(errTopic, clientRequest.method + ' ' + clientRequest.url, + false, { + errorMessage: err.message.toString() + }, + clientRequest.id); + + return Utils.delay(RdGlobalConfig.RETRY_DELAY) + .then(function () { + return RequestLib.call(params, clientRequest, retries - 1, false); + }); + } else { + throw err; + } + }); + } +}; + +module.exports = RequestLib; + diff --git a/src/requestsDebugger.js b/src/requestsDebugger.js new file mode 100644 index 0000000..765e76e --- /dev/null +++ b/src/requestsDebugger.js @@ -0,0 +1,153 @@ +/** + * Entry point for setting up of Requests Debugger Tool. + * Initiates actions such as processing of args, setting up loggers, + * initiating all connectivity checks and stats collection before starting + * the proxy tool. + */ + +var constants = require('../config/constants'); +var LogFiles = constants.LOGS; +var RdGlobalConfig = constants.RdGlobalConfig; +var STATIC_MESSAGES = constants.STATIC_MESSAGES; +var CommandLineManager = require('./commandLine'); +var ConnectivityChecker = require('./connectivity'); +var RdHandler = require('./server'); +var StatsFactory = require('./stats/statsFactory'); +var LogManager = require('./logger'); +var fs = require('fs'); +var path = require('path'); +var Utils = require('./utils'); + +var RdTool = { + + /** + * Initialize the logging directory by resolving the path. + * Exits if there was an error in creating the logs folder at that path. + */ + _initLoggingDirectory: function () { + var basePath = RdGlobalConfig.logsPath ? path.resolve(RdGlobalConfig.logsPath) : process.cwd(); + RdGlobalConfig.LOGS_DIRECTORY = path.resolve(basePath, constants.LOGS_FOLDER); + + if (RdGlobalConfig.DELETE_EXISTING_LOGS) { + var filesToDelete = Object.keys(LogFiles).map(function (key) { return LogFiles[key]; }); + filesToDelete.forEach(function (file) { + try { + fs.unlinkSync(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, file)); + /* eslint-disable-next-line no-empty */ + } catch (e) {} + }); + } + + try { + fs.mkdirSync(RdGlobalConfig.LOGS_DIRECTORY); + } catch (e) { + if (e.code !== constants.ERROR_CODES.EEXIST) { + var errorMessage = "Error in creating " + constants.LOGS_FOLDER + " folder at path: " + basePath + "\n" + + "Message: " + e.toString() + "\n"; + console.log(errorMessage); + process.exit(1); + } + } + }, + + /** + * Initializes the Loggers. + */ + _initLoggers: function () { + RdGlobalConfig.networkLogger = LogManager.initializeLogger(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, LogFiles.NETWORK)); + RdGlobalConfig.memLogger = LogManager.initializeLogger(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, LogFiles.MEM)); + RdGlobalConfig.cpuLogger = LogManager.initializeLogger(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, LogFiles.CPU)); + RdGlobalConfig.reqLogger = LogManager.initializeLogger(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, LogFiles.REQUESTS)); + RdGlobalConfig.connLogger = LogManager.initializeLogger(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, LogFiles.CONNECTIVITY)); + RdGlobalConfig.errLogger = LogManager.initializeLogger(path.resolve(RdGlobalConfig.LOGS_DIRECTORY, LogFiles.ERROR)); + }, + + /** + * Initialize the handlers for specific loggers. Wrappers over statsHandler to perform the + * task and log using the loggers. + */ + _initLoggingHandlers: function () { + RdGlobalConfig.statsHandler = StatsFactory.getHandler(process.platform); + + RdGlobalConfig.networkLogHandler = function (topic, uuid, callback) { + topic = topic || constants.TOPICS.NO_TOPIC; + RdGlobalConfig.statsHandler.network(function (networkStats) { + RdGlobalConfig.networkLogger.info(topic, networkStats, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + }); + }; + + RdGlobalConfig.cpuLogHandler = function (topic, uuid, callback) { + topic = topic || constants.TOPICS.NO_TOPIC; + RdGlobalConfig.statsHandler.cpu(function (cpuStats) { + RdGlobalConfig.cpuLogger.info(topic, cpuStats, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + }); + }; + + RdGlobalConfig.memLogHandler = function (topic, uuid, callback) { + topic = topic || constants.TOPICS.NO_TOPIC; + RdGlobalConfig.statsHandler.mem(function (memStats) { + RdGlobalConfig.memLogger.info(topic, memStats, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + }); + }; + + RdGlobalConfig.connHandler = ConnectivityChecker.fireChecks; + }, + + /** + * Calls internal methods to set up logs directory, loggers and handlers + */ + initLoggersAndHandlers: function () { + RdTool._initLoggingDirectory(); + RdTool._initLoggers(); + RdTool._initLoggingHandlers(); + }, + + /** + * Entry point of the Tool. Scans CLI args, sets up loggers, fires stats + * collection and connectivity checks. Finally, sets up the tool proxy + */ + start: function () { + CommandLineManager.processArgs(process.argv); + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.STARTING_TOOL, '-', '-', 60, true)); + RdTool.initLoggersAndHandlers(); + /* eslint-disable indent */ + console.log(Utils.formatAndBeautifyLine("Refer '" + RdGlobalConfig.LOGS_DIRECTORY + "' folder for CPU/Network/Memory" + + " Stats and Connectivity Checks with BrowserStack components", + '', '-', 60, true)); + /*eslint-enable indent*/ + + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.CHECK_CPU_STATS, '', '-', 60, true)); + RdGlobalConfig.cpuLogHandler('Initial CPU', null, function () { + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.CPU_STATS_COLLECTED, '', '-', 60, true)); + }); + + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.CHECK_NETWORK_STATS, '', '-', 60, true)); + RdGlobalConfig.networkLogHandler('Initial Network', null, function () { + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.NETWORK_STATS_COLLECTED, '', '-', 60, true)); + }); + + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.CHECK_MEMORY_STATS, '', '-', 60, true)); + RdGlobalConfig.memLogHandler('Initial Memory', null, function () { + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.MEMORY_STATS_COLLECTED, '', '-', 60, true)); + }); + + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.CHECK_CONNECTIVITY, '', '-', 60, true)); + RdGlobalConfig.connHandler('Initial Connectivity', null, function () { + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.CONNECTIVITY_CHECKS_DONE, '', '-', 60, true)); + }); + + RdHandler.startProxy(RdGlobalConfig.RD_HANDLER_PORT, function (err, result) { + if (err) { + console.log(STATIC_MESSAGES.ERR_STARTING_TOOL, err); + console.log('Exiting the Tool...'); + process.exit(1); + } + console.log(Utils.formatAndBeautifyLine(STATIC_MESSAGES.TOOL_STARTED_ON_PORT + result, '', '-', 60, true)); + }); + } +}; + +RdTool.start(); diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..d518cae --- /dev/null +++ b/src/server.js @@ -0,0 +1,211 @@ +/** + * Server to Intercept the client's requests and handle them on their behalf. + * Initiates stats and connectivity checks when a requests fails. + * It also responds in selenium understandable error when a request fails + * at tool. + */ + +var http = require('http'); +var url = require('url'); +var uuidv4 = require('uuid/v4'); +var Utils = require('./utils'); +var constants = require('../config/constants'); +var ReqLib = require('./requestLib'); +var RdGlobalConfig = constants.RdGlobalConfig; + +var RdHandler = { + + _requestCounter: 0, + + /** + * Generates the request options template for firing requests based on + * whether the user had provided any proxy input or not. + */ + generatorForRequestOptionsObject: function () { + RdHandler._reqObjTemplate = { + method: null, + headers: {}, + host: null, + port: null, + path: null + }; + + if (RdGlobalConfig.proxy) { + RdHandler._reqObjTemplate.host = RdGlobalConfig.proxy.host; + RdHandler._reqObjTemplate.port = RdGlobalConfig.proxy.port; + + if (RdGlobalConfig.proxy.username && RdGlobalConfig.proxy.password) { + RdHandler._reqObjTemplate.headers['Proxy-Authorization'] = Utils.proxyAuthToBase64(RdGlobalConfig.proxy); + } + + /** + * Sets the internal method to generate request options if external/upstream + * proxy exists + * @param {http.IncomingMessage} clientRequest + * @returns {Object} + */ + RdHandler._generateRequestOptions = function (clientRequest) { + var parsedClientUrl = url.parse(clientRequest.url); + var headersCopy = Object.assign({}, clientRequest.headers, RdHandler._reqObjTemplate.headers); + var requestOptions = Object.assign({}, RdHandler._reqObjTemplate); + requestOptions.path = parsedClientUrl.href; + requestOptions.method = clientRequest.method; + requestOptions.headers = headersCopy; + return requestOptions; + }; + } else { + + /** + * Sets the internal method to generate request options if external/upstream proxy + * doesn't exists + * @param {http.IncomingMessage} clientRequest + * @returns {Object} + */ + RdHandler._generateRequestOptions = function (clientRequest) { + var parsedClientUrl = url.parse(clientRequest.url); + var requestOptions = Object.assign({}, RdHandler._reqObjTemplate); + requestOptions.host = parsedClientUrl.hostname; + requestOptions.port = parsedClientUrl.port || 80; + requestOptions.path = parsedClientUrl.path; + requestOptions.method = clientRequest.method; + requestOptions.headers = clientRequest.headers; + if (parsedClientUrl.auth) { + requestOptions.headers['authorization'] = Utils.proxyAuthToBase64(parsedClientUrl.auth); + } + return requestOptions; + }; + } + }, + + /** + * Frames the error response based on the type of request. + * i.e., if its a request originating for Hub, the response + * is in the format which the client binding would understand. + * @param {Object} parsedRequest + * @param {String} errorMessage + */ + _frameErrorResponse: function (parsedRequest, errorMessage) { + errorMessage += '. ' + constants.STATIC_MESSAGES.REQ_FAILED_MSG; + var parseSessionId = parsedRequest.path.match(/\/wd\/hub\/session\/([a-z0-9]+)\/*/); + if (parseSessionId) { + var sessionId = parseSessionId[1]; + return { + data: { + sessionId: sessionId, + status: 13, + value: { + message: errorMessage, + error: constants.STATIC_MESSAGES.REQ_FAILED_MSG + }, + state: 'error' + }, + statusCode: 500 + }; + } else { + return { + data: { + message: errorMessage, + error: constants.STATIC_MESSAGES.REQ_FAILED_MSG + }, + statusCode: 500 + }; + } + }, + + /** + * Handler for incoming requests to Requests Debugger Tool proxy server. + * @param {http.IncomingMessage} clientRequest + * @param {http.ServerResponse} clientResponse + */ + requestHandler: function (clientRequest, clientResponse) { + clientRequest.id = ++RdHandler._requestCounter + '::' + uuidv4(); + + var request = { + method: clientRequest.method, + url: clientRequest.url, + headers: clientRequest.headers, + data: [] + }; + + RdGlobalConfig.reqLogger.info(constants.TOPICS.CLIENT_REQUEST_START, request.method + ' ' + request.url, + false, { + headers: request.headers + }, + clientRequest.id); + + var furtherRequestOptions = RdHandler._generateRequestOptions(clientRequest); + + var paramsForRequest = { + request: request, + furtherRequestOptions: furtherRequestOptions + }; + + ReqLib.call(paramsForRequest, clientRequest) + .then(function (response) { + RdGlobalConfig.reqLogger.info(constants.TOPICS.CLIENT_RESPONSE_END, clientRequest.method + ' ' + clientRequest.url + ', Status Code: ' + response.statusCode, + false, { + data: response.data, + headers: response.headers, + }, + clientRequest.id); + + clientResponse.writeHead(response.statusCode, response.headers); + clientResponse.end(response.data); + }) + .catch(function (err) { + RdGlobalConfig.reqLogger.error(err.customTopic || constants.TOPICS.UNEXPECTED_ERROR, clientRequest.method + ' ' + clientRequest.url, + false, { + errorMessage: err.message.toString() + }, + clientRequest.id); + + var errorResponse = RdHandler._frameErrorResponse(furtherRequestOptions, err.message.toString()); + RdGlobalConfig.reqLogger.error(constants.TOPICS.CLIENT_RESPONSE_END, clientRequest.method + ' ' + clientRequest.url + ', Status Code: ' + errorResponse.statusCode, + false, + errorResponse.data, + clientRequest.id); + + clientResponse.writeHead(errorResponse.statusCode); + clientResponse.end(JSON.stringify(errorResponse.data)); + }); + }, + + /** + * Starts the proxy server on the given port + * @param {String|Number} port + * @param {Function} callback + */ + startProxy: function (port, callback) { + try { + RdHandler.generatorForRequestOptionsObject(); + RdHandler.server = http.createServer(RdHandler.requestHandler); + RdHandler.server.listen(port); + RdHandler.server.on('listening', function () { + callback(null, port); + }); + RdHandler.server.on('error', function (err) { + callback(err.toString(), null); + }); + } catch (e) { + callback(e.toString(), null); + } + }, + + /** + * Stops the currently running proxy server + * @param {Function} callback + */ + stopProxy: function (callback) { + try { + if (RdHandler.server) { + RdHandler.server.close(); + RdHandler.server = null; + } + callback(null, true); + } catch (e) { + callback(e.toString(), null); + } + } +}; + +module.exports = RdHandler; diff --git a/src/stats/baseStats.js b/src/stats/baseStats.js new file mode 100644 index 0000000..69f8239 --- /dev/null +++ b/src/stats/baseStats.js @@ -0,0 +1,26 @@ +/** + * Base stats object which is inherited by objects of other platforms for generating their + * stats object. + */ + +var Utils = require('../utils'); +var STATIC_MESSAGES = require('../../config/constants').STATIC_MESSAGES; + +var BaseStats = { + description: STATIC_MESSAGES.BASE_STATS_DESC, + + cpu: function (callback) { + if (Utils.isValidCallback(callback)) callback(STATIC_MESSAGES.CPU_STATS_NOT_IMPLEMENTED); + }, + + mem: function (callback) { + if (Utils.isValidCallback(callback)) callback(STATIC_MESSAGES.MEM_STATS_NOT_IMPLEMENTED); + }, + + network: function (callback) { + if (Utils.isValidCallback(callback)) callback(STATIC_MESSAGES.NETWORK_STATS_NOT_IMPLEMENTED); + } + +}; + +module.exports = BaseStats; diff --git a/src/stats/linuxStats.js b/src/stats/linuxStats.js new file mode 100644 index 0000000..6e8b7df --- /dev/null +++ b/src/stats/linuxStats.js @@ -0,0 +1,76 @@ +/** + * Stats Object for fetching information from Linux platform. + * It implements the inherited functions. + */ + +var os = require('os'); +var BaseStats = require('./baseStats'); +var cp = require('child_process'); +var fs = require('fs'); +var Utils = require('../utils'); +var constants = require('../../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; + +var LinuxStats = Object.create(BaseStats); +LinuxStats.description = constants.STATIC_MESSAGES.LINUX_STATS_DESC; + +LinuxStats.cpu = function (callback) { + var startTime = new Date(); + cp.exec(constants.LINUX.TOP_3_SAMPLES, function (err, result) { + if (!err) { + result = result.toString().replace(/top -/g, '\n****************** ITERATION ******************\ntop -'); + result = Utils.generateHeaderAndFooter(result, 'CPU Information with 3 samples', new Date(), startTime); + } + if (Utils.isValidCallback(callback)) callback(result || constants.STATIC_MESSAGES.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); +}; + +LinuxStats.mem = function (callback) { + var memStats = { + total: os.totalmem(), + free: os.freemem(), + swapTotal: 0, + swapUsed: 0, + swapFree: 0 + }; + + memStats.used = memStats.total - memStats.free; + + fs.readFile(constants.LINUX.PROC_MEMINFO, function (err, fileContent) { + if (!err) { + try { + var memStatLines = fileContent.toString().split('\n'); + memStats.total = parseInt(Utils.fetchPropertyValue(memStatLines, 'memtotal')); + memStats.total = memStats.total ? memStats.total * 1024 : os.totalmem(); + memStats.free = parseInt(Utils.fetchPropertyValue(memStatLines, 'memfree')); + memStats.free = memStats.free ? memStats.free * 1024 : os.freemem(); + memStats.used = memStats.total - memStats.free; + + memStats.swapTotal = parseInt(Utils.fetchPropertyValue(memStatLines, 'swaptotal')); + memStats.swapTotal = memStats.swapTotal ? memStats.swapTotal * 1024 : 0; + memStats.swapFree = parseInt(Utils.fetchPropertyValue(memStatLines, 'swapfree')); + memStats.swapFree = memStats.swapFree ? memStats.swapFree * 1024 : 0; + memStats.swapUsed = memStats.swapTotal - memStats.swapFree; + } catch (e) { + RdGlobalConfig.errLogger.error(constants.TOPICS.LINUX_MEM, e.toString(), false, {}); + } + } + if (Utils.isValidCallback(callback)) callback(Utils.beautifyObject(memStats, "Memory", "Bytes")); + }); +}; + +LinuxStats.network = function (callback) { + var startTime = new Date(); + var commands = [constants.LINUX.TCP_LISTEN_ESTABLISHED, constants.COMMON.PING_HUB, constants.COMMON.PING_AUTOMATE]; + var finalOutput = ""; + + Utils.execMultiple(commands, function (results) { + for (var i = 0; i < results.length; i++) { + finalOutput = finalOutput + Utils.generateHeaderAndFooter(results[i].content, "Network Stat: '" + commands[i] + "'", results[i].generatedAt, startTime); + } + + if (Utils.isValidCallback(callback)) callback(finalOutput); + }); +}; + +module.exports = LinuxStats; diff --git a/src/stats/macStats.js b/src/stats/macStats.js new file mode 100644 index 0000000..7307003 --- /dev/null +++ b/src/stats/macStats.js @@ -0,0 +1,87 @@ +/** + * Stats object for fetching information from Mac platform. + * It implements the inherited functions. + */ + +var os = require('os'); +var BaseStats = require('./baseStats'); +var cp = require('child_process'); +var Utils = require('../utils'); +var constants = require('../../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; + +var MacStats = Object.create(BaseStats); +MacStats.description = constants.STATIC_MESSAGES.MAC_STATS_DESC; + +MacStats.cpu = function (callback) { + var startTime = new Date(); + cp.exec(constants.MAC.TOP_3_SAMPLES, function (err, result) { + if (!err) { + result = result.toString().replace(/Processes:/g, '\n****************** ITERATION ******************\nProcesses:'); + result = Utils.generateHeaderAndFooter(result, 'CPU Information with 3 samples', new Date(), startTime); + } + if (Utils.isValidCallback(callback)) callback(result || constants.STATIC_MESSAGES.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); +}; + +MacStats.mem = function (callback) { + + var memStats = { + total: os.totalmem(), + free: os.freemem(), + swapTotal: 0, + swapUsed: 0, + swapFree: 0 + }; + + memStats.used = memStats.total - memStats.free; + + cp.exec(constants.MAC.SWAP_USAGE, function (err, result) { + if (!err) { + try { + var resultLines = result.toString().split('\n'); + if (resultLines[0]) { + var statLines = resultLines[0].trim().split(' '); + for (var index in statLines) { + var swapStatType = statLines[index].toLowerCase().match(/total|used|free/i); + /* eslint-disable indent */ + switch (swapStatType && swapStatType[0]) { + case 'total': + memStats.swapTotal = parseFloat(statLines[index].split('=')[1].trim()) * 1024 * 1024; + break; + + case 'used': + memStats.swapUsed = parseFloat(statLines[index].split('=')[1].trim()) * 1024 * 1024; + break; + + case 'free': + memStats.swapFree = parseFloat(statLines[index].split('=')[1].trim()) * 1024 * 1024; + break; + } + /* eslint-enable indent */ + } + } + } catch (e) { + RdGlobalConfig.errLogger.error(constants.TOPICS.MAC_MEM, e.toString(), false, {}); + } + } + if (Utils.isValidCallback(callback)) callback(Utils.beautifyObject(memStats, "Memory", "Bytes")); + }); +}; + +MacStats.network = function (callback) { + var startTime = new Date(); + var commands = [constants.MAC.TCP_LISTEN_ESTABLISHED, constants.COMMON.PING_HUB, constants.COMMON.PING_AUTOMATE]; + var finalOutput = ""; + + Utils.execMultiple(commands, function (results) { + for (var i = 0; i < results.length; i++) { + finalOutput = finalOutput + Utils.generateHeaderAndFooter(results[i].content, "Network Stat: '" + commands[i] + "'", results[i].generatedAt, startTime); + } + + if (Utils.isValidCallback(callback)) callback(finalOutput); + }); +}; + +module.exports = MacStats; + diff --git a/src/stats/statsFactory.js b/src/stats/statsFactory.js new file mode 100644 index 0000000..7801a5b --- /dev/null +++ b/src/stats/statsFactory.js @@ -0,0 +1,25 @@ +/** + * Factory pattern handler generator for stats collection + * based on the platform + */ + +var MacStats = require('./macStats'); +var WinStats = require('./winStats'); +var LinuxStats = require('./linuxStats'); +var BaseStats = require('./baseStats'); + +var HANDLER_MAPPING = { + 'linux': LinuxStats, + 'darwin': MacStats, + 'win': WinStats +}; + +var StatsFactory = { + getHandler: function (type) { + type = type.match(/linux|darwin|win/) || []; + var handler = HANDLER_MAPPING[type[0]] || BaseStats; + return handler; + } +}; + +module.exports = StatsFactory; diff --git a/src/stats/winStats.js b/src/stats/winStats.js new file mode 100644 index 0000000..fe26c90 --- /dev/null +++ b/src/stats/winStats.js @@ -0,0 +1,82 @@ +/** + * Stats object for fetching information from Windows platforms + * It implements the inherited functions. + */ + +var os = require('os'); +var BaseStats = require('./baseStats'); +var cp = require('child_process'); +var Utils = require('../utils'); +var constants = require('../../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; + +var WinStats = Object.create(BaseStats); +WinStats.description = constants.STATIC_MESSAGES.WIN_STATS_DESC; +WinStats.wmicPath = null; + +// Need to add better CPU stats here. Preferably loadavg like linux/unix. +WinStats.cpu = function (callback) { + WinStats.wmicPath = WinStats.wmicPath || Utils.getWmicPath(); + var startTime = new Date(); + + cp.exec(WinStats.wmicPath + constants.WIN.LOAD_PERCENTAGE, function (err, result) { + if (!err) { + result = Utils.generateHeaderAndFooter(result, "Load Percentage", new Date(), startTime); + } + if (Utils.isValidCallback(callback)) callback(result || constants.STATIC_MESSAGES.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); +}; + +WinStats.mem = function (callback) { + + var memStats = { + total: os.totalmem(), + free: os.freemem(), + swapTotal: 0, + swapUsed: 0, + swapFree: 0 + }; + + memStats.used = memStats.total - memStats.free; + + WinStats.wmicPath = WinStats.wmicPath || Utils.getWmicPath(); + + cp.exec(WinStats.wmicPath + constants.WIN.SWAP_USAGE, function (err, result) { + if (!err) { + try { + result = result.split('\r\n').filter(function (line) { return line.trim() !== ''; }); + result.shift(); + var swapTotal = 0; + var swapUsed = 0; + for (var index in result) { + var line = result[index].trim().split(/\s\s+/); + swapTotal += parseInt(line[0]); + swapUsed += parseInt(line[1]); + } + memStats.swapTotal = swapTotal * 1024 * 1024; + memStats.swapUsed = swapUsed * 1024 * 1024; + memStats.swapFree = memStats.swapTotal - memStats.swapUsed; + } catch (e) { + RdGlobalConfig.errLogger(constants.TOPICS.WIN_MEM, e.toString(), false, {}); + } + } + if (Utils.isValidCallback(callback)) callback(Utils.beautifyObject(memStats, "Memory", "Bytes")); + }); +}; + +WinStats.network = function (callback) { + var startTime = new Date(); + var commands = [constants.WIN.NETSTAT_TCP, constants.WIN.NETSTAT_ROUTING_TABLE, constants.WIN.IPCONFIG_ALL, constants.WIN.PING_HUB, constants.WIN.PING_AUTOMATE]; + var finalOutput = ""; + + Utils.execMultiple(commands, function (results) { + for (var i = 0; i < results.length; i++) { + finalOutput = finalOutput + Utils.generateHeaderAndFooter(results[i].content, "Network Stat: '" + commands[i] + "'", results[i].generatedAt, startTime); + } + + if (Utils.isValidCallback(callback)) callback(finalOutput); + }); +}; + +module.exports = WinStats; + diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..e71821a --- /dev/null +++ b/src/utils.js @@ -0,0 +1,276 @@ +var cp = require('child_process'); +var os = require('os'); +var fs = require('fs'); + +/** + * Returns the value for authorization header by performing + * base64 operation on the proxy auth params. + * Input can be in the form of: + * 1. 'user:pass' + * 2. { username: 'user', password: 'pass' } + * @param {String|{username: String, password: String, host: String, port: String|Number}} proxyObj + * @returns {String} + */ +var proxyAuthToBase64 = function (proxyObj) { + var base64Auth; + if (typeof proxyObj === 'object') { + base64Auth = new Buffer(proxyObj.username + ":" + proxyObj.password); + } else if (typeof proxyObj === 'string') { + base64Auth = new Buffer(proxyObj); + } + return "Basic " + base64Auth.toString('base64'); +}; + +/** + * Fetch the property value from the string array of content, each separated by a separator + * @param {Array} content + * @param {String} propertyToFetch + * @param {String} separator + * @returns {String} + */ +var fetchPropertyValue = function (content, propertyToFetch, separator) { + separator = separator || ':'; + propertyToFetch = propertyToFetch.toLowerCase(); + for (var index in content) { + var modifiedLine = content[index].toLowerCase().replace(/\t/g, ''); + if (modifiedLine.startsWith(propertyToFetch)) { + var splitModifiedLine = modifiedLine.split(separator); + if (splitModifiedLine.length >= 2) { + splitModifiedLine.shift(); + return splitModifiedLine.join(separator).trim(); + } else { + return ''; + } + } + } + return ''; +}; + +/** + * Beautifies the lines and add prefix/suffix characters to make the line of the required length. + * @param {String} line + * @param {String} prefix + * @param {String} suffix + * @param {Number} idealLength + * @param {Boolean} newLine + * @returns {String} + */ +var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine) { + line = safeToString(line); + if (line) { + var lineLength = line.length; + idealLength = idealLength || 70; + newLine = newLine || false; + if (lineLength > idealLength) return (newLine ? line + os.EOL : line); + + var remainingCharacters = idealLength - lineLength; + var prefixCharacters = parseInt(remainingCharacters/2); + var suffixCharacters = remainingCharacters % 2 == 0 ? prefixCharacters : prefixCharacters + 1; + if (prefix && suffix) { + line = prefix.toString().repeat(prefixCharacters) + " " + line + " " + suffix.toString().repeat(suffixCharacters); + } else if (prefix) { + line = prefix.toString().repeat(remainingCharacters) + " " + line; + } else if (suffix) { + line = line + " " + suffix.toString().repeat(remainingCharacters); + } + } + return newLine ? line + os.EOL : line; +}; + +/** + * Generates header and footer for the given content. + * @param {String} content + * @param {String} title + * @param {Date} generatedAt + * @param {Date} startTime + * @returns {String} + */ +var generateHeaderAndFooter = function (content, title, generatedAt, startTime) { + if (typeof content === 'undefined' || !content.toString()) return 'NO_CONTENT_PROVIDED'; + title = title || "NO_TITLE_PROVIDED"; + + startTime = new Date(startTime); + startTime = startTime.toString === 'Invalid Date' ? new Date().toISOString() : startTime.toISOString(); + + generatedAt = new Date(generatedAt); + generatedAt = generatedAt.toString === 'Invalid Date' ? startTime : generatedAt.toISOString(); + + content = os.EOL + formatAndBeautifyLine("=", "*", "*", 90, true) + + formatAndBeautifyLine("Title: " + title, "", "=", 90, true) + + formatAndBeautifyLine("Start Time: " + startTime, "", "=", 90, true) + + formatAndBeautifyLine("Generated At: " + generatedAt, "", "=", 90, true) + + formatAndBeautifyLine("=", "*", "*", 90, true) + + content.toString() + os.EOL + + formatAndBeautifyLine("=", "*", "*", 90, true); + + return content; +}; + +/** + * Performs multiple exec commands asynchronously and returns the + * result in the same order of the commands array. + * @param {Array} commands + * @param {Function} callback + * @returns {Array} + */ +var execMultiple = function (commands, callback) { + if (!Array.isArray(commands)) { + throw Error("COMMANDS_IS_NOT_AN_ARRAY"); + } + + var resultArray = new Array(commands.length); + var totalCommandsCompleted = 0; + + commands.forEach(function (cmd, index) { + cp.exec(cmd, function (err, result) { + if (!err) { + resultArray[index] = { content: result, generatedAt: new Date() }; + } else { + resultArray[index] = { content: "NO_RESULT_GENERATED" + os.EOL, generatedAt: new Date() }; + } + + if (++totalCommandsCompleted === commands.length) { + if (isValidCallback(callback)) callback(resultArray); + } + }); + }); +}; + +/** + * Fetches the WMIC path in Windows + * @returns {String} + */ +var getWmicPath = function () { + if (os.type() === 'Windows_NT') { + var wmicPath = process.env.WINDIR + '\\system32\\wbem\\wmic.exe'; + if (!fs.existsSync(wmicPath)) { + try { + var whereWmicArray = cp.execSync('WHERE WMIC').toString().split('\r\n'); + if (whereWmicArray && whereWmicArray[0]) { + wmicPath = whereWmicArray[0]; + } else { + wmicPath = 'wmic'; + } + } catch (e) { + wmicPath = 'wmic'; + } + } + return wmicPath + ' '; + } else { + throw Error('Not Windows Platform'); + } +}; + +/** + * Beautifies the whole object and returns in a format which can be logged and read easily. + * Can take an object or array of objects as input. + * @param {Object|Array} obj + * @param {String} keysTitle + * @param {String} valuesTitle + * @param {Number} maxKeyLength Optional + * @param {Number} maxValLength Optional + * @returns {String} + */ +var beautifyObject = function (obj, keysTitle, valuesTitle, maxKeyLength, maxValLength) { + if (typeof obj !== 'object') return 'Not an Object' + os.EOL; + if (Array.isArray(obj)) { + var longestKeyOfAll = 0; + var longestValOfAll = 0; + for (var index in obj) { + if (typeof obj[index] !== 'object' && !Array.isArray(obj[index])) continue; + var indObjKeyLength = getLongestVal(Object.keys(obj[index])); + var indObjValLength = getLongestVal(Object.keys(obj[index]).map(function (key) { return obj[index][key]; })); + longestKeyOfAll = indObjKeyLength > longestKeyOfAll ? indObjKeyLength : longestKeyOfAll; + longestValOfAll = indObjValLength > longestValOfAll ? indObjValLength : longestValOfAll; + } + + var aggResult = ''; + for (var ind in obj) { + aggResult += beautifyObject(obj[ind], keysTitle, valuesTitle, longestKeyOfAll, longestValOfAll); + } + return os.EOL + aggResult; + } + + keysTitle = keysTitle || "KEY"; + valuesTitle = valuesTitle || "VALUE"; + var longestKey = maxKeyLength || getLongestVal(Object.keys(obj)); + var longestVal = maxValLength || getLongestVal(Object.keys(obj).map(function (key) { return obj[key]; })); + + longestKey = keysTitle.length > longestKey ? keysTitle.length : longestKey; + longestVal = valuesTitle.length > longestVal ? valuesTitle.length : longestVal; + + var finalResult = formatAndBeautifyLine(keysTitle, " ", " ", longestKey, false) + + ' : ' + + formatAndBeautifyLine(valuesTitle, " ", " ", longestVal, true) + + formatAndBeautifyLine("-", "-", "", longestKey + longestVal, true); + + Object.keys(obj).forEach(function (key) { + finalResult += formatAndBeautifyLine(key, " ", " ", longestKey, false) + + ' : ' + + formatAndBeautifyLine(obj[key], " ", " ", longestVal, true); + }); + + return os.EOL + finalResult + os.EOL; +}; + +/** + * Returns the length of the longest entry in the Array + * @param {Array} arr + * @returns {Number} + */ +var getLongestVal = function (arr) { + var longest = arr.reduce(function (prevValue, currValue) { + if (safeToString(currValue).length > prevValue) { + return safeToString(currValue).length; + } + return prevValue; + }, 0); + return longest; +}; + +/** + * Returns string value by trying .toString() & JSON.stringify() + * @param {any} val + */ +var safeToString = function (val) { + try { + val = val.toString() || 'empty/no data'; + } catch (e) { + val = JSON.stringify(val) || 'undefined'; + } + return val; +}; + +var isValidCallback = function (checkCallback) { + return (checkCallback && typeof checkCallback === 'function'); +}; + +/** + * Add Delay to async calls + * @param {Number} ms Milliseconds to delay the resolving of Promise + * @returns {Promise} + */ +var delay = function (ms) { + ms = parseFloat(ms); + if (isNaN(ms) || ms < 0) { + ms = 0; + } + return new Promise(function (resolve) { + setTimeout(function () { + resolve(); + }, ms); + }); +}; + +module.exports = { + proxyAuthToBase64: proxyAuthToBase64, + fetchPropertyValue: fetchPropertyValue, + formatAndBeautifyLine: formatAndBeautifyLine, + generateHeaderAndFooter: generateHeaderAndFooter, + execMultiple: execMultiple, + getWmicPath: getWmicPath, + beautifyObject: beautifyObject, + isValidCallback: isValidCallback, + safeToString: safeToString, + delay: delay +}; diff --git a/test/commandLine.test.js b/test/commandLine.test.js new file mode 100644 index 0000000..915a883 --- /dev/null +++ b/test/commandLine.test.js @@ -0,0 +1,330 @@ +var CommandLineManager = require('../src/commandLine'); +var constants = require('../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; +var expect = require('chai').expect; +var sinon = require('sinon'); +var testHelper = require('./testHelper'); + +describe('CommandLineManager', function () { + + var argv; + + before(function () { + console.log("NOTE: 'console.log' will be stubbed. In case any test fails, try removing the stub to see the logs"); + }); + + after(function () { + console.log("NOTE: 'console.log' is restored to its original functionality"); + }); + + beforeEach(function () { + argv = ['node', 'file/path/file.js']; + sinon.stub(process, 'exit'); + testHelper.deleteProxy(); + }); + + afterEach(function () { + process.exit.restore(); + }); + + context('Process Arguments', function () { + + it('parse proxy-host and proxy-port params', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', 'host', '--proxy-port', '9687']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy.host).to.eql('host'); + expect(RdGlobalConfig.proxy.port).to.eql(9687); + }); + + it('proxy-port is set to the default value when its not in the expected range', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', 'host', '--proxy-port', '99999']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy.host).to.eql('host'); + expect(RdGlobalConfig.proxy.port).to.eql(constants.DEFAULT_PROXY_PORT); + }); + + it('parse proxy-host, proxy-port, proxy-user and proxy-pass', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', 'host', '--proxy-port', '9687', '--proxy-user', 'user', '--proxy-pass', 'pass']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy.host).to.eql('host'); + expect(RdGlobalConfig.proxy.port).to.eql(9687); + expect(RdGlobalConfig.proxy.username).to.eql('user'); + expect(RdGlobalConfig.proxy.password).to.eql('pass'); + }); + + it('default proxy port if only proxy host is provided', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', 'host']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy.host).to.eql('host'); + expect(RdGlobalConfig.proxy.port).to.eql(constants.DEFAULT_PROXY_PORT); + }); + + it('empty proxy password if only proxy username is provided', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', 'host', '--proxy-port', '9687', '--proxy-user', 'user']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy.host).to.eql('host'); + expect(RdGlobalConfig.proxy.port).to.eql(9687); + expect(RdGlobalConfig.proxy.username).to.eql('user'); + expect(RdGlobalConfig.proxy.password).to.eql(''); + }); + + it("proxy won't be set if proxy host is not provided", function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-port', '9687']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy).to.eql(undefined); + sinon.assert.called(process.exit); + }); + + it("proxy auth won't be set if proxy username is not provided", function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', 'host', '--proxy-port', '9687', '--proxy-pass', 'pass']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.proxy.host).to.eql('host'); + expect(RdGlobalConfig.proxy.port).to.eql(9687); + expect(RdGlobalConfig.proxy.username).to.eql(undefined); + expect(RdGlobalConfig.proxy.password).to.eql(undefined); + }); + + it('defaults to no deletion of existing logs if argument is not provided', function () { + sinon.stub(console, 'log'); + argv = argv.concat([]); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.DELETE_EXISTING_LOGS).to.be.false; + }); + + it('set to true if argument if provided, i.e. existing logs will be deleted', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--del-logs']); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.DELETE_EXISTING_LOGS).to.be.true; + }); + + it("logs help regarding arguments if '--help' is passed irrespective of other arguments", function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--help', '--random-argument']); + CommandLineManager.processArgs(argv); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it("logs version of the tool if '--version' is passed and exits", function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--version']); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, 'Version:', constants.VERSION); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it("invalid args will be logged and the tool will exit", function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--wrongArg']); + CommandLineManager.processArgs(argv); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('args (which require values) without values will lead to invalid args and exiting the process', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-host', '--proxy-port']); + CommandLineManager.processArgs(argv); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('if only --proxy-user is provided instead of the host, it will exit the process with missing args', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-user', 'user']); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nMissing Argument(s): ', '--proxy-host', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('if --proxy-user is passed without any value, it will mark it as invalid and exit the process', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-user']); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--proxy-user', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('if --proxy-pass is passed without any value, it will mark it as invalid and exit the process', function () { + sinon.stub(console, 'log'); + argv = argv.concat(['--proxy-pass']); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--proxy-pass', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('If no args are passed, it will initiate the tool without any external proxy and append to existing logs', function () { + CommandLineManager.processArgs([]); + sinon.assert.notCalled(process.exit); + expect(RdGlobalConfig.proxy).to.be.undefined; + expect(RdGlobalConfig.DELETE_EXISTING_LOGS).to.be.false; + }); + + it('--logs-path arg with value', function () { + argv = argv.concat(['--logs-path', 'randomPath']); + CommandLineManager.processArgs(argv); + expect(RdGlobalConfig.logsPath).to.eql('randomPath'); + }); + + it('marks --logs-path as invalid if the value is not provided with the arg and exits the process', function () { + argv = argv.concat(['--logs-path']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + // --port + it("sets the port of Requests Debugger Tool Proxy using the '--port' argument", function () { + argv = argv.concat(['--port', '9098']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.RD_HANDLER_PORT).to.eql(9098); + }); + + it('Uses the default port of Requests Debugger Tool Proxy if not provided via arguments', function () { + sinon.stub(console, 'log'); + var portBeforeParsing = RdGlobalConfig.RD_HANDLER_PORT; + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.RD_HANDLER_PORT).to.eql(portBeforeParsing); + }); + + it("exits with invalid args if port provided doesn't lie in the Max Min Range", function () { + argv = argv.concat(['--port', '99999']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--port', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('exits with invalid args if the port provided is not a number', function () { + argv = argv.concat(['--port', 'random string']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--port', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('exits with invalid args if the port arg is provided without any value', function () { + argv = argv.concat(['--port']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--port', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + // --request-timeout + it("sets the timeout for the request being fired from the tool using the arg --request-timeout", function () { + argv = argv.concat(['--request-timeout', '200000']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.CLIENT_REQ_TIMEOUT).to.eql(200000); + }); + + it('Uses the default timeout for requests fired from the tool if not provided via arguments', function () { + sinon.stub(console, 'log'); + var timeoutBeforeParsing = RdGlobalConfig.CLIENT_REQ_TIMEOUT; + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.CLIENT_REQ_TIMEOUT).to.eql(timeoutBeforeParsing); + }); + + it("exits with invalid args if request timeout provided is negative", function () { + argv = argv.concat(['--request-timeout', '-1']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--request-timeout', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('exits with invalid args if the request timeout provided is not a number', function () { + argv = argv.concat(['--request-timeout', 'random string']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--request-timeout', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('exits with invalid args if the --request-timeout arg is provided without any value', function () { + argv = argv.concat(['--request-timeout']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--request-timeout', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + // --retry-delay + it("sets the delay after which a failed request should be retried using the arg --retry-delay", function () { + argv = argv.concat(['--retry-delay', '200']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.RETRY_DELAY).to.eql(200); + }); + + it('Uses the default delay before firing the same request again if the arg --retry-delay is not provided', function () { + sinon.stub(console, 'log'); + var delayBeforeParsing = RdGlobalConfig.RETRY_DELAY; + CommandLineManager.processArgs(argv); + console.log.restore(); + expect(RdGlobalConfig.RETRY_DELAY).to.eql(delayBeforeParsing); + }); + + it("exits with invalid args if the delay value provided is negative", function () { + argv = argv.concat(['--retry-delay', '-1']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--retry-delay', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('exits with invalid args if the delay value provided is not a number', function () { + argv = argv.concat(['--retry-delay', 'random string']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--retry-delay', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + + it('exits with invalid args if the --retry-delay arg is provided without any value', function () { + argv = argv.concat(['--retry-delay']); + sinon.stub(console, 'log'); + CommandLineManager.processArgs(argv); + sinon.assert.calledWith(console.log, '\nInvalid Argument(s): ', '--retry-delay', '\n'); + console.log.restore(); + sinon.assert.called(process.exit); + }); + }); +}); diff --git a/test/connectivity.test.js b/test/connectivity.test.js new file mode 100644 index 0000000..819851e --- /dev/null +++ b/test/connectivity.test.js @@ -0,0 +1,166 @@ +var ConnectivityChecker = require('../src/connectivity'); +var constants = require('../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; +var Utils = require('../src/utils'); +var nock = require('nock'); +var sinon = require('sinon'); +var testHelper = require('./testHelper'); + +describe('Connectivity Checker for BrowserStack Components', function () { + + before(function () { + testHelper.initializeDummyLoggers(); + }); + + after(function () { + testHelper.deleteLoggers(); + }); + + var resultWithoutProxy = [{ + data: '{"data":"value"}', + statusCode: 200, + errorMessage: null, + description: 'HTTP Request To Hub Without Proxy', + result: 'Passed' + }, { + data: '{"data":"value"}', + statusCode: 301, + errorMessage: null, + description: 'HTTP Request To Rails Without Proxy', + result: 'Passed' + }, { + data: '{"data":"value"}', + statusCode: 200, + errorMessage: null, + description: 'HTTPS Request To Hub Without Proxy', + result: 'Passed' + }, { + data: '{"data":"value"}', + statusCode: 302, + errorMessage: null, + description: 'HTTPS Request to Rails Without Proxy', + result: 'Passed' + }]; + + var errorResult = [{ + data: [], + statusCode: null, + errorMessage: 'Error: something terrible', + description: 'HTTP Request To Hub Without Proxy', + result: 'Failed' + }, { + data: [], + statusCode: null, + errorMessage: 'Error: something terrible', + description: 'HTTP Request To Rails Without Proxy', + result: 'Failed' + }, { + data: [], + statusCode: null, + errorMessage: 'Error: something terrible', + description: 'HTTPS Request To Hub Without Proxy', + result: 'Failed' + }, { + data: [], + statusCode: null, + errorMessage: 'Error: something terrible', + description: 'HTTPS Request to Rails Without Proxy', + result: 'Failed' + }]; + + context('without Proxy', function () { + beforeEach(function () { + testHelper.deleteProxy(); + ConnectivityChecker.connectionChecks = []; + testHelper.nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + testHelper.nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); + testHelper.nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); + testHelper.nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + it('HTTP(S) to Hub & Rails', function (done) { + this.timeout(2000); + sinon.stub(Utils, 'beautifyObject'); + + ConnectivityChecker.fireChecks("some topic", 1, function () { + sinon.assert.calledOnceWithExactly(Utils.beautifyObject, resultWithoutProxy, "Result Key", "Result Value"); + Utils.beautifyObject.restore(); + done(); + }); + }); + }); + + context('with Proxy', function () { + beforeEach(function () { + testHelper.initializeDummyProxy(); + ConnectivityChecker.connectionChecks = []; + testHelper.nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + testHelper.nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); + testHelper.nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); + testHelper.nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); + testHelper.nockProxyUrl(RdGlobalConfig.proxy, 'http', 'hub', null, 200); + testHelper.nockProxyUrl(RdGlobalConfig.proxy, 'http', 'automate', null, 301); + }); + + afterEach(function () { + nock.cleanAll(); + testHelper.deleteProxy(); + }); + + it('HTTP(S) to Hub & Rails', function (done) { + this.timeout(2000); + sinon.stub(Utils, 'beautifyObject'); + var resultWithProxy = resultWithoutProxy.concat([{ + data: '{"data":"value"}', + description: "HTTP Request To Hub With Proxy", + errorMessage: null, + result: "Passed", + statusCode: 200 + }, { + data: '{"data":"value"}', + description: "HTTP Request To Rails With Proxy", + errorMessage: null, + result: "Passed", + statusCode: 301 + }]); + + ConnectivityChecker.fireChecks("some topic", 1, function () { + sinon.assert.calledOnceWithExactly(Utils.beautifyObject, resultWithProxy, "Result Key", "Result Value"); + Utils.beautifyObject.restore(); + done(); + }); + }); + }); + + // similar case as non error scenario. The only difference is to trigger the 'error' event of request + // Thus, no need to show it for 'with proxy' case + context('without Proxy error case', function () { + beforeEach(function () { + testHelper.deleteProxy(); + ConnectivityChecker.connectionChecks = []; + testHelper.nockGetRequestWithError(constants.HUB_STATUS_URL, 'http'); + testHelper.nockGetRequestWithError(constants.HUB_STATUS_URL, 'https'); + testHelper.nockGetRequestWithError(constants.RAILS_AUTOMATE, 'http'); + testHelper.nockGetRequestWithError(constants.RAILS_AUTOMATE, 'https'); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + it('HTTP(S) to Hub & Rails', function (done) { + this.timeout(2000); + sinon.stub(Utils, 'beautifyObject'); + + ConnectivityChecker.fireChecks("some topic", 1, function () { + sinon.assert.calledOnceWithExactly(Utils.beautifyObject, errorResult, "Result Key", "Result Value"); + Utils.beautifyObject.restore(); + done(); + }); + }); + }); +}); diff --git a/test/logger.test.js b/test/logger.test.js new file mode 100644 index 0000000..488df72 --- /dev/null +++ b/test/logger.test.js @@ -0,0 +1,117 @@ +var LogManager = require('../src/logger'); +var winston = require('winston'); +var sinon = require('sinon'); +var expect = require('chai').expect; +var assert = require('chai').assert; + +describe('LogManager', function () { + var mockLogger; + + beforeEach(function () { + mockLogger = { + transports: { + file: { + filename: 'randomFileName.log' + } + }, + info: sinon.spy(), + error: sinon.spy() + }; + sinon.stub(winston, 'Logger').returns(mockLogger); + }); + + afterEach(function () { + winston.Logger.restore(); + }); + + context('Winston Logger', function () { + it('returns winston logger with transports set to file', function () { + var logger = LogManager.getLogger('randomFileName.log'); + expect(logger.transports.file.filename).to.eql('randomFileName.log'); + }); + }); + + context('initializeLogger', function () { + it("initializes logger and returns 'info' and 'error' functions", function () { + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + sinon.assert.calledOnceWithExactly(mockLogger.info, "************* LOGGER INITIALIZED **************\r\n"); + expect(Object.keys(randomLogger)).to.eql(['info', 'error']); + }); + + it("'info' returned by the initializeLogger calls the 'info' of winston logger in formatted manner with message", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.info("randomTopic", "message", false, {}, 1); + sinon.assert.calledTwice(mockLogger.info); + assert.equal(mockLogger.info.firstCall.calledWith("************* LOGGER INITIALIZED **************\r\n"), true); + assert.equal(mockLogger.info.secondCall.calledWith("message, ", { topic: 'randomTopic', uuid: 1 }), true); + LogManager.getLogger.restore(); + }); + + it("'info' returned by the initializeLogger calls the 'info' of winston logger in formatted manner with message stringified", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.info("randomTopic", "message", true, {}, 1); + sinon.assert.calledTwice(mockLogger.info); + assert.equal(mockLogger.info.firstCall.calledWith("************* LOGGER INITIALIZED **************\r\n"), true); + assert.equal(mockLogger.info.secondCall.calledWith(JSON.stringify("message") + ', ', { topic: 'randomTopic', uuid: 1 }), true); + LogManager.getLogger.restore(); + }); + + it("'info' returned by the initializeLogger calls the 'info' of winston logger in formatted manner with message and data", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.info("randomTopic", "message", false, { some: "data" }, 1); + sinon.assert.calledTwice(mockLogger.info); + assert.equal(mockLogger.info.firstCall.calledWith("************* LOGGER INITIALIZED **************\r\n"), true); + assert.equal(mockLogger.info.secondCall.calledWith("message, " + JSON.stringify({"some":"data"}), { topic: 'randomTopic', uuid: 1 }), true); + LogManager.getLogger.restore(); + }); + + it("'info' returned by the initializeLogger calls the 'info' of winston logger in formatted manner without topic and uuid", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.info("", "message", false, { some: "data" }); + sinon.assert.calledTwice(mockLogger.info); + assert.equal(mockLogger.info.firstCall.calledWith("************* LOGGER INITIALIZED **************\r\n"), true); + assert.equal(mockLogger.info.secondCall.calledWith("message, " + JSON.stringify({"some":"data"}), { topic: '', uuid: '' }), true); + LogManager.getLogger.restore(); + }); + + it("'error' returned by the initializeLogger calls the 'error' of winston logger in formatted manner with message", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.error("randomTopic", "message", false, {}, 1); + sinon.assert.calledOnce(mockLogger.error); + assert.equal(mockLogger.error.firstCall.calledWith("message, ", { topic: 'randomTopic', uuid: 1 }), true); + LogManager.getLogger.restore(); + }); + + it("'error' returned by the initializeLogger calls the 'error' of winston logger in formatted manner with message stringified", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.error("randomTopic", "message", true, {}, 1); + sinon.assert.calledOnce(mockLogger.error); + assert.equal(mockLogger.error.firstCall.calledWith(JSON.stringify("message") + ', ', { topic: 'randomTopic', uuid: 1 }), true); + LogManager.getLogger.restore(); + }); + + it("'error' returned by the initializeLogger calls the 'error' of winston logger in formatted manner with message and data", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.error("randomTopic", "message", false, { some: "data" }, 1); + sinon.assert.calledOnce(mockLogger.error); + assert.equal(mockLogger.error.firstCall.calledWith("message, " + JSON.stringify({"some":"data"}), { topic: 'randomTopic', uuid: 1 }), true); + LogManager.getLogger.restore(); + }); + + it("'error' returned by the initializeLogger calls the 'error' of winston logger in formatted manner without topic and uuid", function () { + sinon.stub(LogManager, 'getLogger').returns(mockLogger); + var randomLogger = LogManager.initializeLogger('randomFileName.log'); + randomLogger.error("", "message", false, { some: "data" }); + sinon.assert.calledOnce(mockLogger.error); + assert.equal(mockLogger.error.firstCall.calledWith("message, " + JSON.stringify({"some":"data"}), { topic: '', uuid: '' }), true); + LogManager.getLogger.restore(); + }); + }); +}); diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..5307bd7 --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,120 @@ +var constants = require('../config/constants'); +var RdGlobalConfig = constants.RdGlobalConfig; +var nock = require('nock'); +var RdHandler = require('../src/server'); +var http = require('http'); +var assert = require('chai').assert; +var testHelper = require('./testHelper'); + +describe('RdHandler', function () { + context('Proxy Server', function () { + + before(function (done) { + this.timeout = 5000; + testHelper.nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + testHelper.initializeDummyLoggers(); + testHelper.initializeDummyHandlers(); + + RdHandler.startProxy(RdGlobalConfig.RD_HANDLER_PORT, function (port) { + console.log('Test Network Utility Proxy Started on Port: ', port); + done(); + }); + }); + + after(function (done) { + this.timeout = 5000; + RdHandler.stopProxy(function () { + done(); + }); + testHelper.deleteLoggers(); + testHelper.deleteHandlers(); + nock.cleanAll(); + }); + + it('Requests on behalf of the client and returns the response', function (done) { + this.timeout = 5000; + var reqOptions = { + method: 'GET', + host: 'localhost', + port: RdGlobalConfig.RD_HANDLER_PORT, + headers: {}, + path: constants.HUB_STATUS_URL + }; + + var responseData = []; + var request = http.request(reqOptions, function (response) { + + response.on('data', function (chunk) { + responseData.push(chunk); + }); + + response.on('end', function () { + assert(Buffer.concat(responseData).toString() === '{"data":"value"}'); + done(); + }); + }); + + request.end(); + }); + + it('Requests on behalf of the client via external proxy and returns the response', function (done) { + this.timeout = 5000; + testHelper.initializeDummyProxy(); + testHelper.nockProxyUrl(RdGlobalConfig.proxy, 'http', 'hub', null, 200); + RdHandler.generatorForRequestOptionsObject(); + var reqOptions = { + method: 'GET', + host: 'localhost', + port: RdGlobalConfig.RD_HANDLER_PORT, + headers: {}, + path: constants.HUB_STATUS_URL + }; + + var responseData = []; + var request = http.request(reqOptions, function (response) { + + response.on('data', function (chunk) { + responseData.push(chunk); + }); + + response.on('end', function () { + assert(Buffer.concat(responseData).toString() === '{"data":"value"}'); + done(); + }); + }); + + request.end(); + testHelper.deleteProxy(); + }); + + it('Requests on behalf of the client via external proxy and returns the response even if request by tool fails', function (done) { + this.timeout = 5000; + for (var i = 0; i <= constants.MAX_RETRIES; i++) { + testHelper.nockGetRequestWithError(constants.HUB_STATUS_URL, 'http'); + } + RdHandler.generatorForRequestOptionsObject(); + var reqOptions = { + method: 'GET', + host: 'localhost', + port: RdGlobalConfig.RD_HANDLER_PORT, + headers: {}, + path: constants.HUB_STATUS_URL + }; + + var responseData = []; + var request = http.request(reqOptions, function (response) { + + response.on('data', function (chunk) { + responseData.push(chunk); + }); + + response.on('end', function () { + assert(Buffer.concat(responseData).toString() === '{"message":"Error: something terrible. Request Failed At Requests Debugger","error":"Request Failed At Requests Debugger"}'); + done(); + }); + }); + + request.end(); + }); + }); +}); diff --git a/test/stats/baseStats.test.js b/test/stats/baseStats.test.js new file mode 100644 index 0000000..4304369 --- /dev/null +++ b/test/stats/baseStats.test.js @@ -0,0 +1,24 @@ +var BaseStats = require('../../src/stats/baseStats'); +var expect = require('chai').expect; + +describe('BaseStats', function () { + context('Default Functions', function () { + it("should callback with 'CPU Stats Not Yet Implemented' for cpu function", function () { + BaseStats.cpu(function (result) { + expect(result).to.eql('CPU Stats Not Yet Implemented'); + }); + }); + + it("should callback with 'Mem Stats Not Yet Implemented' for mem function", function () { + BaseStats.mem(function (result) { + expect(result).to.eql('Mem Stats Not Yet Implemented'); + }); + }); + + it("should callback with 'Network Stats Not Yet Implemented' for network function", function () { + BaseStats.network(function (result) { + expect(result).to.eql('Network Stats Not Yet Implemented'); + }); + }); + }); +}); diff --git a/test/stats/linuxStats.test.js b/test/stats/linuxStats.test.js new file mode 100644 index 0000000..63ba6dd --- /dev/null +++ b/test/stats/linuxStats.test.js @@ -0,0 +1,116 @@ +var LinuxStats = require('../../src/stats/linuxStats'); +var os = require('os'); +var constants = require('../../config/constants'); +var expect = require('chai').expect; +var sinon = require('sinon'); +var cp = require('child_process'); +var fs = require('fs'); +var Utils = require('../../src/utils'); + +describe('LinuxStats', function () { + context('CPU stats', function () { + it('callbacks with the result of cpu stats', function () { + var stats = "CPU Stats Generated"; + var statsWithHeaderFooter = "Header" + os.EOL + stats + os.EOL + "Footer" + os.EOL; + + sinon.stub(cp, 'exec').callsArgWith(1, null, stats); + sinon.stub(Utils, 'generateHeaderAndFooter').returns(statsWithHeaderFooter); + + LinuxStats.cpu(function (result) { + expect(result).to.eql(statsWithHeaderFooter); + }); + + cp.exec.restore(); + Utils.generateHeaderAndFooter.restore(); + }); + + it('callbacks with proper message when no stats are available', function () { + sinon.stub(cp, 'exec').callsArgWith(1, "err", null); + + LinuxStats.cpu(function (result) { + expect(result).to.eql(constants.STATIC_MESSAGES.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); + + cp.exec.restore(); + }); + }); + + context('Mem Stats', function () { + it('callbacks with result of mem stats', function () { + var stats = "MemTotal:102400 KB\nMemFree:51200 KB\nSwapTotal:102400 KB\nSwapFree:51200 KB"; + sinon.stub(os, 'totalmem').returns(100 * 1024 * 1024); + sinon.stub(os, 'freemem').returns(50 * 1024 * 1024); + sinon.stub(Utils, 'beautifyObject'); + sinon.stub(fs, 'readFile').callsArgWith(1, null, stats); + + var memStats = { + total: 100 * 1024 * 1024, + free: 50 * 1024 * 1024, + used: 50 * 1024 * 1024, + swapTotal: 100 * 1024 * 1024, + swapUsed: 50 * 1024 * 1024, + swapFree: 50 * 1024 * 1024 + }; + /* eslint-disable-next-line no-unused-vars */ + LinuxStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + Utils.beautifyObject.restore(); + fs.readFile.restore(); + }); + + it('callbacks with the total, free & used mem stats except swap if error occurs in fs command', function () { + sinon.stub(fs, 'readFile').callsArgWith(1, "err", null); + sinon.stub(Utils, 'beautifyObject'); + sinon.stub(os, 'totalmem').returns(100 * 1024 * 1024); + sinon.stub(os, 'freemem').returns(50 * 1024 * 1024); + + var memStats = { + total: 100 * 1024 * 1024, + free: 50 * 1024 * 1024, + used: 50 * 1024 * 1024, + swapTotal: 0, + swapUsed: 0, + swapFree: 0 + }; + + /* eslint-disable-next-line no-unused-vars */ + LinuxStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + fs.readFile.restore(); + Utils.beautifyObject.restore(); + }); + }); + + context('Network Stats', function () { + it('callbacks with the stats content of multiple commands', function () { + var results = [{ + content: 'resultOne', + generatedAt: new Date().toISOString() + }, { + content: 'resultTwo', + generatedAt: new Date().toISOString() + }, { + content: 'resultThree', + generatedAt: new Date().toISOString() + }]; + + sinon.stub(Utils, 'execMultiple').callsArgWith(1, results); + sinon.stub(Utils, 'generateHeaderAndFooter').returns('headerFooterContent'); + + LinuxStats.network(function (result) { + sinon.assert.calledThrice(Utils.generateHeaderAndFooter); + expect(result).to.eql('headerFooterContent'.repeat(3)); + }); + Utils.generateHeaderAndFooter.restore(); + Utils.execMultiple.restore(); + }); + }); +}); diff --git a/test/stats/macStats.test.js b/test/stats/macStats.test.js new file mode 100644 index 0000000..d2e3266 --- /dev/null +++ b/test/stats/macStats.test.js @@ -0,0 +1,114 @@ +var MacStats = require('../../src/stats/macStats'); +var os = require('os'); +var constants = require('../../config/constants'); +var expect = require('chai').expect; +var sinon = require('sinon'); +var cp = require('child_process'); +var Utils = require('../../src/utils'); + +describe('MacStats', function () { + context('CPU Stats', function () { + it('callbacks with the result of cpu stats', function () { + var stats = "CPU Stats Generated"; + var statsWithHeaderFooter = "Header" + os.EOL + stats + os.EOL + "Footer" + os.EOL; + + sinon.stub(cp, 'exec').callsArgWith(1, null, stats); + sinon.stub(Utils, 'generateHeaderAndFooter').returns(statsWithHeaderFooter); + + MacStats.cpu(function (result) { + expect(result).to.eql(statsWithHeaderFooter); + }); + + cp.exec.restore(); + Utils.generateHeaderAndFooter.restore(); + }); + + it('callbacks with proper message when no stats are available', function () { + sinon.stub(cp, 'exec').callsArgWith(1, "err", null); + + MacStats.cpu(function (result) { + expect(result).to.eql(constants.STATIC_MESSAGES.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); + + cp.exec.restore(); + }); + }); + + context('Mem Stats', function () { + it('callbacks with the result of mem stats', function () { + var stats = "Total=100 Used=50 Free=50\n"; + sinon.stub(os, 'totalmem').returns(100 * 1024 * 1024); + sinon.stub(os, 'freemem').returns(50 * 1024 * 1024); + sinon.stub(Utils, 'beautifyObject'); + sinon.stub(cp, 'exec').callsArgWith(1, null, stats); + + var memStats = { + total: 100 * 1024 * 1024, + free: 50 * 1024 * 1024, + used: 50 * 1024 * 1024, + swapTotal: 100 * 1024 * 1024, + swapUsed: 50 * 1024 * 1024, + swapFree: 50 * 1024 * 1024 + }; + /* eslint-disable-next-line no-unused-vars */ + MacStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + Utils.beautifyObject.restore(); + cp.exec.restore(); + }); + + it('callbacks with the total, free & used mem stats except swap if error occurs in exec command', function () { + sinon.stub(cp, 'exec').callsArgWith(1, "err", null); + sinon.stub(Utils, 'beautifyObject'); + sinon.stub(os, 'totalmem').returns(100 * 1024 * 1024); + sinon.stub(os, 'freemem').returns(50 * 1024 * 1024); + + var memStats = { + total: 100 * 1024 * 1024, + free: 50 * 1024 * 1024, + used: 50 * 1024 * 1024, + swapTotal: 0, + swapUsed: 0, + swapFree: 0 + }; + /* eslint-disable-next-line no-unused-vars */ + MacStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + cp.exec.restore(); + Utils.beautifyObject.restore(); + }); + }); + + context('Network Stats', function () { + it('callbacks with the stats content of multiple commands', function () { + var results = [{ + content: 'resultOne', + generatedAt: new Date().toISOString() + }, { + content: 'resultTwo', + generatedAt: new Date().toISOString() + }, { + content: 'resultThree', + generatedAt: new Date().toISOString() + }]; + + sinon.stub(Utils, 'execMultiple').callsArgWith(1, results); + sinon.stub(Utils, 'generateHeaderAndFooter').returns('headerFooterContent'); + + MacStats.network(function (result) { + sinon.assert.calledThrice(Utils.generateHeaderAndFooter); + expect(result).to.eql('headerFooterContent'.repeat(3)); + }); + Utils.generateHeaderAndFooter.restore(); + Utils.execMultiple.restore(); + }); + }); +}); diff --git a/test/stats/statsFactory.test.js b/test/stats/statsFactory.test.js new file mode 100644 index 0000000..6880c49 --- /dev/null +++ b/test/stats/statsFactory.test.js @@ -0,0 +1,26 @@ +var StatsFactory = require('../../src/stats/statsFactory'); +var expect = require('chai').expect; + +describe('StatsFactory', function () { + context('Fetches handler based on the platform provided', function () { + it('Mac Platform Handler', function () { + var MacHandler = StatsFactory.getHandler('darwin'); + expect(MacHandler.description).to.eql('System and Network Stats for Mac'); + }); + + it('Win Platform Handler', function () { + var WinHandler = StatsFactory.getHandler('win'); + expect(WinHandler.description).to.eql('System and Network Stats for Windows'); + }); + + it('Linux Stats Handler', function () { + var LinuxHandler = StatsFactory.getHandler('linux'); + expect(LinuxHandler.description).to.eql('System and Network Stats for Linux'); + }); + + it('Generic Stats Handler, i.e. Base Stats', function () { + var BaseStats = StatsFactory.getHandler('randomPlatform'); + expect(BaseStats.description).to.eql('Base Object for System & Network Stats'); + }); + }); +}); diff --git a/test/stats/winStats.test.js b/test/stats/winStats.test.js new file mode 100644 index 0000000..c3f1bef --- /dev/null +++ b/test/stats/winStats.test.js @@ -0,0 +1,129 @@ +var WinStats = require('../../src/stats/winStats'); +var os = require('os'); +var constants = require('../../config/constants'); +var expect = require('chai').expect; +var sinon = require('sinon'); +var cp = require('child_process'); +var Utils = require('../../src/utils'); + +describe('WinStats', function () { + beforeEach(function () { + WinStats.wmicPath = null; + sinon.stub(Utils, 'getWmicPath').returns('path/to/wmic.exe '); + }); + + afterEach(function () { + Utils.getWmicPath.restore(); + }); + + context('CPU Stats', function () { + it('callbacks with the result of cpu stats', function () { + var stats = "CPU Stats Generated"; + var statsWithHeaderFooter = "Header" + os.EOL + stats + os.EOL + "Footer" + os.EOL; + + sinon.stub(cp, 'exec').callsArgWith(1, null, stats); + sinon.stub(Utils, 'generateHeaderAndFooter').returns(statsWithHeaderFooter); + + WinStats.cpu(function (result) { + expect(result).to.eql(statsWithHeaderFooter); + }); + + cp.exec.restore(); + Utils.generateHeaderAndFooter.restore(); + }); + + it('callbacks with proper message when no stats are available', function () { + sinon.stub(cp, 'exec').callsArgWith(1, "err", null); + + WinStats.cpu(function (result) { + expect(result).to.eql(constants.STATIC_MESSAGES.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); + + cp.exec.restore(); + }); + }); + + context('Mem Stats', function () { + it('callbacks with result of mem stats', function () { + var execOutput = "AllocatedBaseSize CurrentUsage \r\r\n100 50 \r\r\n\r\r\n"; + sinon.stub(Utils, 'beautifyObject'); + sinon.stub(cp, 'exec').callsArgWith(1, null, execOutput); + sinon.stub(os, 'totalmem').returns(100 * 1024 * 1024); + sinon.stub(os, 'freemem').returns(50 * 1024 * 1024); + + var memStats = { + total: 100 * 1024 * 1024, + free: 50 * 1024 * 1024, + used: 50 * 1024 * 1024, + swapTotal: 100 * 1024 * 1024, + swapUsed: 50 * 1024 * 1024, + swapFree: 50 * 1024 * 1024 + }; + /* eslint-disable-next-line no-unused-vars */ + WinStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + cp.exec.restore(); + Utils.beautifyObject.restore(); + }); + + it('callbacks with the total, free & used mem stats except swap if error occurs in fs command', function () { + sinon.stub(Utils, 'beautifyObject'); + sinon.stub(cp, 'exec').callsArgWith(1, 'err', null); + sinon.stub(os, 'totalmem').returns(100 * 1024 * 1024); + sinon.stub(os, 'freemem').returns(50 * 1024 * 1024); + + var memStats = { + total: 100 * 1024 * 1024, + free: 50 * 1024 * 1024, + used: 50 * 1024 * 1024, + swapTotal: 0, + swapUsed: 0, + swapFree: 0 + }; + /* eslint-disable-next-line no-unused-vars */ + WinStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + cp.exec.restore(); + Utils.beautifyObject.restore(); + }); + }); + + context('Network Stats', function () { + it('callbacks with the stats content of multiple commands', function () { + var results = [{ + content: 'resultOne', + generatedAt: new Date().toISOString() + }, { + content: 'resultTwo', + generatedAt: new Date().toISOString() + }, { + content: 'resultThree', + generatedAt: new Date().toISOString() + }, { + content: 'resultFour', + generatedAt: new Date().toISOString() + }, { + content: 'resultFive', + generatedAt: new Date().toISOString() + }]; + + sinon.stub(Utils, 'execMultiple').callsArgWith(1, results); + sinon.stub(Utils, 'generateHeaderAndFooter').returns('headerFooterContent'); + + WinStats.network(function (result) { + sinon.assert.callCount(Utils.generateHeaderAndFooter, 5); + expect(result).to.eql('headerFooterContent'.repeat(5)); + }); + Utils.generateHeaderAndFooter.restore(); + Utils.execMultiple.restore(); + }); + }); +}); diff --git a/test/testHelper.js b/test/testHelper.js new file mode 100644 index 0000000..caa91d6 --- /dev/null +++ b/test/testHelper.js @@ -0,0 +1,125 @@ +var nock = require('nock'); +var url = require('url'); +var constants = require('../config/constants'); + +// capture a request via the given url and path and return the required status code with data +function nockGetRequest(reqUrl, type, data, statusCode) { + data = (data && typeof data === 'object') ? data : { "data": "value" }; + type = (['http', 'https'].indexOf(type) !== -1) ? type : 'http'; + try { + statusCode = parseInt(statusCode); + } catch (e) { + statusCode = 200; + } + + var parsedUrl = url.parse(reqUrl); + var port = parsedUrl.port; + port = port || (type === 'http' ? '80' : '443'); + return nock(type + '://' + parsedUrl.hostname + ':' + port) + .get(parsedUrl.path) + .reply(statusCode, data); +} + +// capture a request which will pass via an upstream proxy and return the desired status code & data +function nockProxyUrl(proxyObj, type, component, data, statusCode) { + type = ['http', 'https'].indexOf(type) ? type : 'http'; + data = (data && typeof data === 'object') ? data : { "data": "value" }; + try { + statusCode = parseInt(statusCode); + } catch (e) { + statusCode = 200; + } + + var proxyUrl = type + '://' + proxyObj.host + ':' + proxyObj.port; + return nock(proxyUrl) + .get(new RegExp(component)) + .reply(statusCode, data); +} + +// capture a request and return error. This gets captured in 'error' event of the request +function nockGetRequestWithError(reqUrl, type) { + type = (['http', 'https'].indexOf(type) !== -1) ? type : 'http'; + + var parsedUrl = url.parse(reqUrl); + var port = parsedUrl.port; + port = port || (type === 'http' ? '80' : '443'); + return nock(type + '://' + parsedUrl.hostname + ':' + port) + .get(parsedUrl.path) + .replyWithError('something terrible'); +} + +// Additional Getters and Setters for Initializing/Restoring Dummy Loggers, Handlers & Proxy +function initializeDummyProxy() { + constants.RdGlobalConfig.proxy = { + host: "dummyhost12345.com", + port: "3128", + username: "user", + password: "pass" + }; +} + +function deleteProxy() { + delete constants.RdGlobalConfig.proxy; +} + +function initializeDummyLoggers() { + constants.RdGlobalConfig.connLogger = { + info: function () {}, + error: function () {} + }; + constants.RdGlobalConfig.networkLogger = { + info: function () {}, + error: function () {} + }; + constants.RdGlobalConfig.memLogger = { + info: function () {}, + error: function () {} + }; + constants.RdGlobalConfig.cpuLogger = { + info: function () {}, + error: function () {} + }; + constants.RdGlobalConfig.reqLogger = { + info: function () {}, + error: function () {} + }; + constants.RdGlobalConfig.errLogger = { + info: function () {}, + error: function () {} + }; +} + +function deleteLoggers() { + delete constants.RdGlobalConfig.connLogger; + delete constants.RdGlobalConfig.networkLogger; + delete constants.RdGlobalConfig.memLogger; + delete constants.RdGlobalConfig.cpuLogger; + delete constants.RdGlobalConfig.reqLogger; + delete constants.RdGlobalConfig.errLogger; +} + +function initializeDummyHandlers() { + constants.RdGlobalConfig.networkLogHandler = function () {}; + constants.RdGlobalConfig.connHandler = function () {}; + constants.RdGlobalConfig.cpuLogHandler = function () {}; + constants.RdGlobalConfig.memLogHandler = function () {}; +} + +function deleteHandlers() { + delete constants.RdGlobalConfig.networkLogHandler; + delete constants.RdGlobalConfig.connHandler; + delete constants.RdGlobalConfig.cpuLogHandler; + delete constants.RdGlobalConfig.memLogHandler; +} + +module.exports = { + nockGetRequest: nockGetRequest, + nockProxyUrl: nockProxyUrl, + nockGetRequestWithError: nockGetRequestWithError, + initializeDummyProxy: initializeDummyProxy, + initializeDummyLoggers: initializeDummyLoggers, + initializeDummyHandlers: initializeDummyHandlers, + deleteProxy: deleteProxy, + deleteHandlers: deleteHandlers, + deleteLoggers: deleteLoggers +}; diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..5d6703f --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,372 @@ + +var chai = require('chai'); +var sinon = require('sinon'); +var Utils = require('../src/utils'); +var os = require('os'); +var fs = require('fs'); +var cp = require('child_process'); +var RdGlobalConfig = require('../config/constants').RdGlobalConfig; +var expect = chai.expect; + +describe('Utils', function () { + context('proxyAuthToBase64', function () { + it('should return the auth header value when proxy object is passed with username and password', function () { + var proxyObj = { + username: "general", + password: "fancy" + }; + var base64 = new Buffer(proxyObj.username + ':' + proxyObj.password).toString('base64'); + expect(Utils.proxyAuthToBase64(proxyObj)).to.eql('Basic ' + base64); + }); + + it('should return the auth header value when auth params are passed in user:pass format', function () { + var auth = 'general:fancy'; + var base64 = new Buffer(auth).toString('base64'); + expect(Utils.proxyAuthToBase64(auth)).to.eql('Basic ' + base64); + }); + }); + + context('fetchPropertyValue', function () { + it('should fetch the given property value where the content is an array of key value pairs separated by a separator', function () { + var content = [ + 'keyOne :valueOne', + 'keyTwo: valueTwo' + ]; + var value = Utils.fetchPropertyValue(content, 'keytwo', ':'); + expect(value).to.eql('valuetwo'); + }); + + it('should return empty string if no key found', function () { + var content = [ + 'keyOne :valueOne', + 'keyTwo: valueTwo' + ]; + var value = Utils.fetchPropertyValue(content, 'keythree', ':'); + expect(value).to.eql(''); + }); + + it('should return empty string if key found with no value', function () { + var content = [ + 'keyOne :valueOne', + 'keyTwo' + ]; + var value = Utils.fetchPropertyValue(content, 'keytwo', ':'); + expect(value).to.eql(''); + }); + }); + + context('formatAndBeautifyLine', function () { + it('should prefix and suffix the line with the given values and make it equal to the given length', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '=', 60, false); + expect(beautifiedLine).to.eql('=========== Hello, This is Requests Debugger Tool ============'); + }); + + it('should prefix the line with the given values and make it equal to the given length', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '', 60, false); + expect(beautifiedLine).to.eql('======================= Hello, This is Requests Debugger Tool'); + }); + + it('should suffix the line with the given values and make it equal to the given length', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 60, false); + expect(beautifiedLine).to.eql('Hello, This is Requests Debugger Tool ======================='); + }); + + it('should prefix and suffix the line with the given values and make it equal to the given length with a newline', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '=', 60, true); + expect(beautifiedLine).to.eql('=========== Hello, This is Requests Debugger Tool ============' + os.EOL); + }); + + it('should prefix the line with the given values and make it equal to the given length with a newline', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '', 60, true); + expect(beautifiedLine).to.eql('======================= Hello, This is Requests Debugger Tool' + os.EOL); + }); + + it('should suffix the line with the given values and make it equal to the given length with a newline', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 60, true); + expect(beautifiedLine).to.eql('Hello, This is Requests Debugger Tool =======================' + os.EOL); + }); + + it('should return the line as it is in case the desired length is lesser than the actual line length', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 10, false); + expect(beautifiedLine).to.eql('Hello, This is Requests Debugger Tool'); + }); + + it('should return the line as it is in case the desired length is lesser than the actual line length with a newline', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 10, true); + expect(beautifiedLine).to.eql('Hello, This is Requests Debugger Tool' + os.EOL); + }); + + it('default ideal length for the string will be taken as 70', function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '='); + expect(beautifiedLine).to.eql('================ Hello, This is Requests Debugger Tool ================='); + }); + + it("default ideal length for the string will be taken as 70, but won't have any effect if the actual line exceeds that length", function () { + var lineToBeautify = "Hello, This is Requests Debugger Tool. And the length exceeds 70 characters."; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '='); + expect(beautifiedLine).to.eql('Hello, This is Requests Debugger Tool. And the length exceeds 70 characters.'); + }); + }); + + context('generateHeaderAndFooter', function () { + it('generates header and footer for the given content', function () { + var startTime = new Date(); + var generatedAt = startTime; + var expectedContent = "\n******************************************** = *********************************************\n" + + "Title: Heading ============================================================================\n" + + "Start Time: " + startTime.toISOString() + " ======================================================\n" + + "Generated At: " + generatedAt.toISOString() + " ====================================================\n" + + "******************************************** = *********************************************\n" + + "Requests Debugger Tool\n" + + "******************************************** = *********************************************\n"; + var content = Utils.generateHeaderAndFooter("Requests Debugger Tool", "Heading", generatedAt, startTime); + expect(content).to.eql(expectedContent); + }); + + it('returns message if not content is provided or if the content is empty', function () { + var startTime = new Date(); + var generatedAt = startTime; + var content = Utils.generateHeaderAndFooter("", "Heading", generatedAt, startTime); + expect(content).to.eql("NO_CONTENT_PROVIDED"); + }); + }); + + context('beautifyObject', function () { + it('generates a beautified version of the object for logging', function () { + var obj = { + "keyOne": "valueOne", + "keyTwo": "valueTwo" + }; + + var expectedOutput = "\n KEYS : VALUES \n" + + "------------- -\n" + + " keyOne : valueOne \n" + + " keyTwo : valueTwo \n\n"; + + var beautifiedObject = Utils.beautifyObject(obj, "KEYS", "VALUES"); + expect(beautifiedObject).to.eql(expectedOutput); + }); + + it('generates a beautified version of array of objects for logging', function () { + var objs = [{ + "keyOne": "valueOne", + "keyTwo": "valueTwo" + }, { + "keyFour": "valueFour" + }]; + + var expectedOutput = "\n\n KEYS : VALUES \n" + + "--------------- -\n" + + " keyOne : valueOne \n" + + " keyTwo : valueTwo \n" + + "\n\n KEYS : VALUES \n" + + "--------------- -\n" + + " keyFour : valueFour \n\n"; + var beautifiedObject = Utils.beautifyObject(objs, "KEYS", "VALUES"); + expect(beautifiedObject).to.eql(expectedOutput); + }); + + it('returns message if no obj is provided to be beautified', function () { + var result = Utils.beautifyObject("random content", "KEYS", "VALUES"); + expect(result).to.eql('Not an Object' + os.EOL); + }); + + it('returns message for each non obj passed in an array', function () { + var objs = [{ + "key": "value" + }, + "wrongInput", + { + "keyTwo": "valueTwo" + }]; + + var expectedOutput = "\n\n KEYS : VALUES \n" + + "------------- -\n" + + " key : value \n\n" + + "Not an Object\n\n" + + " KEYS : VALUES \n" + + "------------- -\n" + + " keyTwo : valueTwo \n\n"; + var result = Utils.beautifyObject(objs, "KEYS", "VALUES"); + expect(result).to.eql(expectedOutput); + }); + }); + + context('safeToString', function () { + it('should return string even if .toString() fails', function () { + var nonStringableValue = null; + var stringifiedValue = Utils.safeToString(nonStringableValue); + expect(stringifiedValue).to.eql('null'); + }); + + it("should return 'undefined' if input is undefined", function () { + var nonStringableValue = undefined; + var stringifiedValue = Utils.safeToString(nonStringableValue); + expect(stringifiedValue).to.eql('undefined'); + }); + }); + + context('isValidCallback', function () { + it('should return true if callback is a function', function () { + var callback = function () {}; + expect(Utils.isValidCallback(callback)).to.be.true; + }); + + it('should return false if callback is other than function', function () { + var callback = 'I am not a function'; + expect(Utils.isValidCallback(callback)).to.be.false; + }); + }); + + context('getWmicPath', function () { + var prevWINDIR = process.env.WINDIR; + beforeEach(function () { + process.env.WINDIR = 'some\\directory'; + }); + + afterEach(function () { + process.env.WINDIR = prevWINDIR; + }); + + it('should return wmic path if file exists at the expected location', function () { + sinon.stub(os, 'type').returns('Windows_NT'); + sinon.stub(fs, 'existsSync').returns(true); + var wmicPath = Utils.getWmicPath(); + expect(wmicPath).to.eql('some\\directory\\system32\\wbem\\wmic.exe '); + os.type.restore(); + fs.existsSync.restore(); + }); + + it("if wmic doesn't exists in the desired location, it will search and return the path", function () { + sinon.stub(os, 'type').returns('Windows_NT'); + sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(cp, 'execSync').returns('some\\directory\\system32\\wbem\\wmic.exe\r\n'); + var wmicPath = Utils.getWmicPath(); + expect(wmicPath).to.eql('some\\directory\\system32\\wbem\\wmic.exe '); + os.type.restore(); + fs.existsSync.restore(); + cp.execSync.restore(); + }); + + it("if wmic doesn't exist in the desired path and is also not found by searching, it shall return 'wmic '", function () { + sinon.stub(os, 'type').returns('Windows_NT'); + sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(cp, 'execSync').returns(''); + var wmicPath = Utils.getWmicPath(); + expect(wmicPath).to.eql('wmic '); + os.type.restore(); + fs.existsSync.restore(); + cp.execSync.restore(); + }); + + it("should return 'wmic ' if searching for path raises an exception", function () { + sinon.stub(os, 'type').returns('Windows_NT'); + sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(cp, 'execSync').returns(null); + var wmicPath = Utils.getWmicPath(); + expect(wmicPath).to.eql('wmic '); + os.type.restore(); + fs.existsSync.restore(); + cp.execSync.restore(); + }); + + it('should throw error if platform is not windows', function () { + sinon.stub(os, 'type').returns('NON_WINDOWS'); + expect(function () { Utils.getWmicPath(); }).to.throw("Not Windows Platform"); + os.type.restore(); + }); + }); + + context('execMultiple', function () { + it('should throw error if the commands provided are not in an array', function () { + var commands = "it's not going to work"; + expect(function () { Utils.execMultiple(commands); }).to.throw("COMMANDS_IS_NOT_AN_ARRAY"); + }); + + it('should execute and return results array', function () { + var commands = ['commandOne', 'commandTwo']; + sinon.stub(cp, 'exec').callsArgWith(1, null, 'executed'); + Utils.execMultiple(commands, function (resultArray) { + resultArray.forEach(function (result) { + expect(result.content).to.eql('executed'); + }); + }); + cp.exec.restore(); + }); + + it('should execute and return results array even if err occurs', function () { + var commands = ['commandOne', 'commandTwo']; + sinon.stub(cp, 'exec').callsArgWith(1, 'some error'); + Utils.execMultiple(commands, function (resultArray) { + resultArray.forEach(function (result) { + expect(result.content).to.eql('NO_RESULT_GENERATED' + os.EOL); + }); + }); + cp.exec.restore(); + }); + }); + + context("Delay for async calls", function () { + it("should resolve the promise after the stated delay in milliseconds", function (done) { + this.timeout = 3000; + var fakeTimer = sinon.useFakeTimers(); + var startTime = Date.now(); + var delayInMS = 2000; + Utils.delay(delayInMS) + .then(function () { + expect((Date.now() - startTime) === delayInMS).to.be.true; + fakeTimer.restore(); + done(); + }); + fakeTimer.tick(delayInMS); + }); + + it("should resolve the promise with default delay if no explicit delay is provided", function (done) { + this.timeout = 3000; + var fakeTimer = sinon.useFakeTimers(); + var startTime = Date.now(); + Utils.delay() + .then(function () { + expect((Date.now() - startTime) === RdGlobalConfig.RETRY_DELAY).to.be.true; + fakeTimer.restore(); + done(); + }); + fakeTimer.tick(RdGlobalConfig.RETRY_DELAY); + }); + + it("should resolve the promise with default delay if invalid explicit delay is provided", function (done) { + this.timeout = 3000; + var fakeTimer = sinon.useFakeTimers(); + var startTime = Date.now(); + Utils.delay("Invalid Value") + .then(function () { + expect((Date.now() - startTime) === RdGlobalConfig.RETRY_DELAY).to.be.true; + fakeTimer.restore(); + done(); + }); + fakeTimer.tick(RdGlobalConfig.RETRY_DELAY); + }); + + it("should resolve the promise with default delay if negative explicit delay is provided", function (done) { + this.timeout = 3000; + var fakeTimer = sinon.useFakeTimers(); + var startTime = Date.now(); + Utils.delay(-1000) + .then(function () { + expect((Date.now() - startTime) === RdGlobalConfig.RETRY_DELAY).to.be.true; + fakeTimer.restore(); + done(); + }); + fakeTimer.tick(RdGlobalConfig.RETRY_DELAY); + }); + }); +});