From c96daca382e0b8949469ec2270633cc04adaf8e2 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Fri, 29 May 2020 14:24:13 +0530 Subject: [PATCH 01/54] Network Utility Tool in a separate folder. Yet to add specs. --- .gitignore | 105 ++++ config/constants.js | 41 ++ package-lock.json | 1039 +++++++++++++++++++++++++++++++++++++ package.json | 17 + src/commandLine.js | 49 ++ src/connectivity.js | 157 ++++++ src/logger.js | 47 ++ src/node.js | 80 +++ src/server.js | 211 ++++++++ src/stats/baseStats.js | 20 + src/stats/linuxStats.js | 67 +++ src/stats/macStats.js | 75 +++ src/stats/statsFactory.js | 19 + src/stats/winStats.js | 73 +++ src/utils.js | 208 ++++++++ 15 files changed, 2208 insertions(+) create mode 100644 .gitignore create mode 100644 config/constants.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/commandLine.js create mode 100644 src/connectivity.js create mode 100644 src/logger.js create mode 100644 src/node.js create mode 100644 src/server.js create mode 100644 src/stats/baseStats.js create mode 100644 src/stats/linuxStats.js create mode 100644 src/stats/macStats.js create mode 100644 src/stats/statsFactory.js create mode 100644 src/stats/winStats.js create mode 100644 src/utils.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a5fe53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +NWT_Logs/ + +# 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 diff --git a/config/constants.js b/config/constants.js new file mode 100644 index 0000000..d7ba8d2 --- /dev/null +++ b/config/constants.js @@ -0,0 +1,41 @@ +module.exports.NO_REPORT_GENERATED = "COULD NOT GENERATE REPORT FOR : "; +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 = 7000; +module.exports.CLIENT_REQ_TIMEOUT = 50000; +module.exports.REQ_TIMED_OUT = "Request Timed Out. Did not get any response for " + this.CLIENT_REQ_TIMEOUT + ' ms.'; +module.exports.REQ_FAILED_MSG = "Request Failed At Network Tool"; +module.exports.LOGS = Object.freeze({ + NETWORK: 'NetworkStats.log', + CPU: 'CPUStats.log', + MEM: 'MemStats.log', + REQUESTS: 'Requests.log', + CONNECTIVITY: 'Connectivity.log' +}); +module.exports.NwtGlobalConfig = {}; +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 +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..21b5036 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1039 @@ +{ + "name": "networkutilitytool", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/parser": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.1.tgz", + "integrity": "sha512-AUTksaz3FqugBkbTZ1i+lDLG5qy8hIzCaAxEtttU6C0BtZZU9pkNZtWSVAht4EW9kl46YBiyTGMp9xTTGqViNg==", + "dev": true + }, + "@babel/runtime": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.1.tgz", + "integrity": "sha512-nQbbCbQc9u/rpg1XCxoMYQTbSMVZjCDxErQ1ClCn9Pvcmv1lGads19ep0a2VsEiIJeHqjZley6EQGEC3Yo1xMA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.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 + }, + "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-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" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "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" + } + }, + "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=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.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 + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "escodegen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", + "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "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 + }, + "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 + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "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-glob": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz", + "integrity": "sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "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 + }, + "fastq": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^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" + } + }, + "globby": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz", + "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "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 + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "ignore": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.6.tgz", + "integrity": "sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "into-stream": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz", + "integrity": "sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA==", + "dev": true, + "requires": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + } + }, + "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-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-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "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=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "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-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 + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "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" + } + }, + "multistream": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", + "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.5" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "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 + }, + "p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pkg": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.4.8.tgz", + "integrity": "sha512-Fqqv0iaX48U3CFZxd6Dq6JKe7BrAWbgRAqMJkz/m8W3H5cqJ6suvsUWe5AJPRlN/AhbBYXBJ0XG9QlYPTXcVFA==", + "dev": true, + "requires": { + "@babel/parser": "^7.9.4", + "@babel/runtime": "^7.9.2", + "chalk": "^3.0.0", + "escodegen": "^1.14.1", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "into-stream": "^5.1.1", + "minimist": "^1.2.5", + "multistream": "^2.1.1", + "pkg-fetch": "^2.6.7", + "progress": "^2.0.3", + "resolve": "^1.15.1", + "stream-meter": "^1.0.4" + } + }, + "pkg-fetch": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.8.tgz", + "integrity": "sha512-CFG7jOeVD38lltLGA7xCJxYsD//GKLjl1P9tc/n9By2a4WEHQjfkBMrYdMS8WOHVP+r9L20fsZNbaKcubDAiQg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.9.2", + "byline": "^5.0.0", + "chalk": "^3.0.0", + "expand-template": "^2.0.3", + "fs-extra": "^8.1.0", + "minimist": "^1.2.5", + "progress": "^2.0.3", + "request": "^2.88.0", + "request-progress": "^3.0.0", + "semver": "^6.3.0", + "unique-temp-dir": "^1.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "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 + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "dev": true, + "requires": { + "throttleit": "^1.0.0" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", + "dev": true, + "requires": { + "readable-stream": "^2.1.4" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "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" + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", + "dev": true + }, + "unique-temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", + "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1", + "os-tmpdir": "^1.0.1", + "uid2": "0.0.3" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "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" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "winston": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", + "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", + "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 + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ebc300 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "networkutilitytool", + "version": "1.0.0", + "description": "Tool to debug failed/dropped requests at client side", + "main": "src/node.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "pkg": "^4.4.8" + }, + "dependencies": { + "winston": "2.4.4" + } +} diff --git a/src/commandLine.js b/src/commandLine.js new file mode 100644 index 0000000..a6efe4f --- /dev/null +++ b/src/commandLine.js @@ -0,0 +1,49 @@ +var NwtGlobalConfig = require('../config/constants').NwtGlobalConfig; + +var CommandLineManager = { + validArgValue: function (value) { + return value && value.length > 0 && !value.startsWith('-'); + }, + + processArgs: function (argv) { + + // Process proxy arguments + var index = argv.indexOf('--proxy-host'); + if (index !== -1 && CommandLineManager.validArgValue(argv[index + 1])) { + NwtGlobalConfig['proxy'] = NwtGlobalConfig['proxy'] || {}; + NwtGlobalConfig.proxy['host'] = argv[index + 1]; + argv.splice(index, 2); + + index = argv.indexOf('--proxy-port'); + if (index !== -1 && CommandLineManager.validArgValue(argv[index + 1])) { + NwtGlobalConfig.proxy['port'] = argv[index + 1]; + argv.splice(index, 2); + } else { + NwtGlobalConfig.proxy['port'] = 3128; + } + + index = argv.indexOf('--proxy-user'); + if (index !== -1 && CommandLineManager.validArgValue(argv[index + 1])) { + NwtGlobalConfig.proxy['username'] = argv[index + 1]; + argv.splice(index, 2); + + index = argv.indexOf('--proxy-pass'); + if (index !== -1 && CommandLineManager.validArgValue(argv[index + 1])) { + NwtGlobalConfig.proxy['password'] = argv[index + 1]; + argv.splice(index, 2); + } else { + NwtGlobalConfig.proxy['password'] = ''; + } + } + } + + var index = argv.indexOf('--del-logs'); + if (index !== -1) { + NwtGlobalConfig.deleteExistingLogs = true; + } else { + NwtGlobalConfig.deleteExistingLogs = false; + } + } +} + +module.exports = CommandLineManager; \ No newline at end of file diff --git a/src/connectivity.js b/src/connectivity.js new file mode 100644 index 0000000..6eef217 --- /dev/null +++ b/src/connectivity.js @@ -0,0 +1,157 @@ +var http = require('http'); +var url = require('url'); +var constants = require('../config/constants'); +var NwtGlobalConfig = constants.NwtGlobalConfig; +var Utils = require('./utils'); +var https = require('https'); + + +var fireRequest = function (requestOptions, requestType, description, successCodes, callback) { + var httpOrHttps = requestType === 'http' ? http : https; + var responseData = { + data: [], + statusCode: null, + errorMessage: null, + 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); + }); + }, + + + 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 (NwtGlobalConfig.proxy) { + ConnectivityChecker.connectionChecks.push(this.httpToHubWithProxy, this.httpToRailsWithProxy); + ConnectivityChecker.reqOpsWithProxy = function (reqUrl, reqType) { + var parsedUrl = url.parse(reqUrl); + var reqOptions = { + method: 'GET', + headers: { + 'Proxy-Authorization': Utils.proxyAuthToBase64(NwtGlobalConfig.proxy) + }, + host: NwtGlobalConfig.proxy.host, + port: NwtGlobalConfig.proxy.port, + path: parsedUrl.href + } + return reqOptions; + } + } + } + }, + + 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"); + NwtGlobalConfig.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..6f1f6d6 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,47 @@ +var winston = require('winston'); + +var LogManager = { + getLogger: function (filename) { + return new (winston.Logger)({ + transports: [ + new (winston.transports.File)({ + 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/node.js b/src/node.js new file mode 100644 index 0000000..fc52179 --- /dev/null +++ b/src/node.js @@ -0,0 +1,80 @@ +var LogFiles = require('../config/constants').LOGS; +var NwtGlobalConfig = require('../config/constants').NwtGlobalConfig; +var CommandLineManager = require('./commandLine'); +var ConnectivityChecker = require('./connectivity'); +var NWTHandler = require('./server'); +var StatsFactory = require('./stats/statsFactory'); +var LogManager = require('./logger'); +var fs = require('fs'); +var path = require('path'); +var Utils = require('./utils'); + +var NwTool = { + initLoggers: function () { + NwtGlobalConfig.LOGS_DIRECTORY = path.resolve(process.cwd(), 'NWT_Logs'); + + if (NwtGlobalConfig.deleteExistingLogs) { + var filesToDelete = Object.keys(LogFiles).map(function (key) { return LogFiles[key]}); + filesToDelete.forEach(function (file) { + try { + fs.unlinkSync(path.resolve(NwtGlobalConfig.LOGS_DIRECTORY, file)) + } catch (e) {} + }); + } + + try { + fs.mkdirSync(NwtGlobalConfig.LOGS_DIRECTORY); + } catch (e) { + if (e.code !== 'EEXIST') { + console.log("Error While Creating NWT_Logs Directory. Exiting with status code 1. Error: ", e); + process.exit(1); + } + } + + NwtGlobalConfig.NetworkLogger = LogManager.initializeLogger(path.resolve(NwtGlobalConfig.LOGS_DIRECTORY, LogFiles.NETWORK)); + NwtGlobalConfig.MemLogger = LogManager.initializeLogger(path.resolve(NwtGlobalConfig.LOGS_DIRECTORY, LogFiles.MEM)); + NwtGlobalConfig.CPULogger = LogManager.initializeLogger(path.resolve(NwtGlobalConfig.LOGS_DIRECTORY, LogFiles.CPU)); + NwtGlobalConfig.ReqLogger = LogManager.initializeLogger(path.resolve(NwtGlobalConfig.LOGS_DIRECTORY, LogFiles.REQUESTS)); + NwtGlobalConfig.ConnLogger = LogManager.initializeLogger(path.resolve(NwtGlobalConfig.LOGS_DIRECTORY, LogFiles.CONNECTIVITY)); + + NwtGlobalConfig.NetworkLogHandler = function (topic, uuid, callback) { + topic = topic || 'NO_TOPIC'; + NwtGlobalConfig.StatsHandler.network(function (networkStats) { + NwtGlobalConfig.NetworkLogger.info(topic, networkStats, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + }); + } + + NwtGlobalConfig.CpuLogHandler = function (topic, uuid, callback) { + topic = topic || 'NO_TOPIC'; + NwtGlobalConfig.StatsHandler.cpu(function (cpuStats) { + NwtGlobalConfig.CPULogger.info(topic, cpuStats, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + }); + } + + NwtGlobalConfig.MemLogHandler = function (topic, uuid, callback) { + topic = topic || "NO_TOPIC"; + NwtGlobalConfig.StatsHandler.mem(function (memStats) { + NwtGlobalConfig.MemLogger.info(topic, memStats, false, {}, uuid); + if (Utils.isValidCallback(callback)) callback(); + }); + } + + NwtGlobalConfig.ConnHandler = ConnectivityChecker.fireChecks; + }, + + start: function () { + CommandLineManager.processArgs(process.argv); + NwtGlobalConfig.StatsHandler = StatsFactory.getHandler(process.platform); + NwTool.initLoggers(); + NwtGlobalConfig.CpuLogHandler("Initial CPU"); + NwtGlobalConfig.NetworkLogHandler("Initial Network"); + NwtGlobalConfig.MemLogHandler("Initial Memory"); + NwtGlobalConfig.ConnHandler("Initial Connectivity", null, function () { + NWTHandler.startProxy(); + }); + } +} + +NwTool.start(); \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..1e7f1a4 --- /dev/null +++ b/src/server.js @@ -0,0 +1,211 @@ +var http = require('http'); +var keepAliveAgent = new http.Agent({ keepAlive: true }); +var url = require('url'); +var Utils = require('./utils'); +var constants = require('../config/constants'); +var NwtGlobalConfig = constants.NwtGlobalConfig; + +var NWTHandler = { + + _requestCounter: 0, + + generatorForRequestOptionsObject: function () { + NWTHandler._reqObjTemplate = { + method: null, + headers: {}, + host: null, + port: null, + path: null + } + + if (NwtGlobalConfig.proxy) { + NWTHandler._reqObjTemplate.host = NwtGlobalConfig.proxy.host; + NWTHandler._reqObjTemplate.port = NwtGlobalConfig.proxy.port; + + if (NwtGlobalConfig.proxy.username && NwtGlobalConfig.proxy.password) { + NWTHandler._reqObjTemplate.headers['Proxy-Authorization'] = Utils.proxyAuthToBase64(NwtGlobalConfig.proxy); + } + + NWTHandler._generateRequestOptions = function (clientRequest) { + var parsedClientUrl = url.parse(clientRequest.url); + var headersCopy = Object.assign({}, clientRequest.headers, NWTHandler._reqObjTemplate.headers); + var requestOptions = Object.assign({}, NWTHandler._reqObjTemplate); + requestOptions.path = parsedClientUrl.href; + requestOptions.method = clientRequest.method; + requestOptions.headers = headersCopy; + return requestOptions; + } + } else { + NWTHandler._generateRequestOptions = function (clientRequest) { + var parsedClientUrl = url.parse(clientRequest.url); + var requestOptions = Object.assign({}, NWTHandler._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; + } + } + }, + + _frameErrorResponse: function (parsedRequest, errorMessage) { + errorMessage += '. ' + constants.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.REQ_FAILED_MSG + }, + state: 'error' + }, + statusCode: 500 + } + } else { + return { + data: { + message: errorMessage, + error: constants.REQ_FAILED_MSG + }, + statusCode: 500 + } + } + }, + + /** + * + * @param {Socket} source + * @param {Socket} destination + * @param {Buffer} chunk + */ + _dataEventHandler: function (source, destination, chunk) { + if (!destination.write(chunk)) { + source.pause(); + destination.once('drain', function () { + source.resume(); + }) + } + }, + + _responseDataHandler: function (source, destination, chunk) { + NWTHandler._dataEventHandler(source, destination, chunk); + }, + + _requestDataHandler: function (source, destination, chunk) { + NWTHandler._dataEventHandler(source, destination, chunk); + }, + + _executeRequest: function (requestOptions, callback) { + var toolToFurtherRequest = http.request(Object.assign({}, requestOptions, { agent: keepAliveAgent }), function (response) { + callback(response); + }); + + return toolToFurtherRequest; + }, + + requestHandler: function (clientRequest, clientResponse) { + clientRequest.id = ++NWTHandler._requestCounter; + + var request = { + method: clientRequest.method, + url: clientRequest.url, + headers: clientRequest.headers, + data: [] + } + + NwtGlobalConfig.ReqLogger.info("Request Start", request.method + ' ' + request.url, false, + { headers: request.headers }, + clientRequest.id); + + var furtherRequestOptions = NWTHandler._generateRequestOptions(clientRequest); + + var response = { + data: [], + statusCode: null, + errorMessage: null, + headers: null + } + + var furtherRequest = NWTHandler._executeRequest(furtherRequestOptions, function (incomingResponse) { + clientResponse.writeHead(incomingResponse.statusCode, incomingResponse.headers); + response.statusCode = incomingResponse.statusCode; + response.headers = incomingResponse.headers; + + incomingResponse.on('data', function (chunk) { + response.data.push(chunk); + NWTHandler._responseDataHandler(incomingResponse, clientResponse, chunk); + }); + + incomingResponse.on('end', function () { + response.data = Buffer.concat(response.data).toString(); + NwtGlobalConfig.ReqLogger.info("Response End", clientRequest.method + ' ' + clientRequest.url + ', Status Code: ' + response.statusCode, + false, + { data: response.data, headers: response.headers, errorMessage: response.errorMessage }, + clientRequest.id); + clientResponse.end(); + }); + }); + + NwtGlobalConfig.ReqLogger.info("Tool Request", clientRequest.method + ' ' + clientRequest.url, false, + furtherRequestOptions, + clientRequest.id); + + furtherRequest.on('error', function (err) { + NwtGlobalConfig.ReqLogger.error("Tool Request", clientRequest.method + ' ' + clientRequest.url, false, + Object.assign({}, furtherRequestOptions, { errorMessage: err.toString() }, { data: Buffer.concat(request.data).toString() }), + clientRequest.id); + + var errorResponse = NWTHandler._frameErrorResponse(furtherRequestOptions, err.toString()); + clientResponse.writeHead(errorResponse.statusCode); + clientResponse.end(JSON.stringify(errorResponse.data)); + + NwtGlobalConfig.NetworkLogHandler("During Further Request", clientRequest.id); + NwtGlobalConfig.ConnHandler("During Further Request", clientRequest.id); + }); + + clientRequest.on('data', function (chunk) { + request.data.push(chunk); + NWTHandler._requestDataHandler(clientRequest, furtherRequest, chunk); + }); + + clientRequest.on('error', function (err) { + NwtGlobalConfig.ReqLogger.error("Request", clientRequest.method + ' ' + clientRequest.url, false, + { headers: request.headers, errorMessage: err.toString() }, + clientRequest.id); + furtherRequest.end(); + + NwtGlobalConfig.NetworkLogHandler("Request", clientRequest.id); + NwtGlobalConfig.ConnHandler("Request", clientRequest.id); + }); + + clientRequest.on('end', function () { + NwtGlobalConfig.ReqLogger.info("Request End", request.method + ' ' + request.url, false, + { data: Buffer.concat(request.data).toString() }, + clientRequest.id); + furtherRequest.end(); + }); + + furtherRequest.setTimeout(constants.CLIENT_REQ_TIMEOUT, function () { + furtherRequest.destroy(constants.REQ_TIMED_OUT); + }); + + }, + + startProxy: function () { + NWTHandler.generatorForRequestOptionsObject(); + var server = http.createServer(NWTHandler.requestHandler); + server.listen(9687, function () { + console.log("Started on 9687"); + }); + } +} + +module.exports = NWTHandler; diff --git a/src/stats/baseStats.js b/src/stats/baseStats.js new file mode 100644 index 0000000..ed09530 --- /dev/null +++ b/src/stats/baseStats.js @@ -0,0 +1,20 @@ +var Utils = require('../utils'); + +var BaseStats = { + description: "Base Object for System & Network Stats.", + + cpu: function (callback) { + if (Utils.isValidCallback(callback)) callback("CPU Stats Not Yet Implemented"); + }, + + mem: function (callback) { + if (Utils.isValidCallback(callback)) callback("Mem Stats Not Yet Implemented"); + }, + + network: function (callback) { + if (Utils.isValidCallback(callback)) callback("Network Stats Not Yet Implemented"); + } + +} + +module.exports = BaseStats; diff --git a/src/stats/linuxStats.js b/src/stats/linuxStats.js new file mode 100644 index 0000000..bfe9fbe --- /dev/null +++ b/src/stats/linuxStats.js @@ -0,0 +1,67 @@ +var os = require('os'); +var BaseStats = require('./baseStats'); +var exec = require('child_process').exec; +var fs = require('fs'); +var Utils = require('../utils'); +var constants = require('../../config/constants'); + +var LinuxStats = Object.create(BaseStats); +LinuxStats.description = "System and Network Related stats for Linux"; + +LinuxStats.cpu = function (callback) { + var startTime = new Date(); + 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.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) { + 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; + } + 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 || constants.NO_REPORT_GENERATED + 'Network'); + }); +} + +module.exports = LinuxStats; diff --git a/src/stats/macStats.js b/src/stats/macStats.js new file mode 100644 index 0000000..3b8b303 --- /dev/null +++ b/src/stats/macStats.js @@ -0,0 +1,75 @@ +var os = require('os'); +var BaseStats = require('./baseStats'); +var exec = require('child_process').exec; +var Utils = require('../utils'); +var constants = require('../../config/constants'); + +var MacStats = Object.create(BaseStats); +MacStats.description = "System and Network Stats for Mac"; + +MacStats.cpu = function (callback) { + var startTime = new Date(); + 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.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; + + exec(constants.MAC.SWAP_USAGE, function (err, result) { + if (!err) { + var resultLines = result.toString().split('\n'); + if (resultLines[0]) { + var statLines = resultLines[0].trim().split(' '); + for (var swapStat of statLines) { + var swapStatType = swapStat.toLowerCase().match(/total|used|free/i); + switch (swapStatType && swapStatType[0]) { + case 'total': + memStats.swapTotal = parseFloat(swapStat.split('=')[1].trim()) * 1024 * 1024; + break; + + case 'used': + memStats.swapUsed = parseFloat(swapStat.split('=')[1].trim()) * 1024 * 1024; + break; + + case 'free': + memStats.swapFree = parseFloat(swapStat.split('=')[1].trim()) * 1024 * 1024; + break; + } + } + } + } + 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 || constants.NO_REPORT_GENERATED + 'Network'); + }); +} + +module.exports = MacStats; + diff --git a/src/stats/statsFactory.js b/src/stats/statsFactory.js new file mode 100644 index 0000000..4821b48 --- /dev/null +++ b/src/stats/statsFactory.js @@ -0,0 +1,19 @@ +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) { + var handler = HANDLER_MAPPING[type] || BaseStats; + return handler; + } +} + +module.exports = StatsFactory; diff --git a/src/stats/winStats.js b/src/stats/winStats.js new file mode 100644 index 0000000..0d379e2 --- /dev/null +++ b/src/stats/winStats.js @@ -0,0 +1,73 @@ +var os = require('os'); +var BaseStats = require('./baseStats'); +var exec = require('child_process').exec; +var fs = require('fs'); +var Utils = require('../utils'); +var constants = require('../../config/constants'); + +var WinStats = Object.create(BaseStats); +WinStats.description = "System and Network Stats for Windows"; +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 = startTime(); + + 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.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(); + + exec(WinStats.wmicPath + constants.WIN.SWAP_USAGE, function (err, result) { + if (!err) { + result = result.split('\r\n').filter(function (line) { return line.trim() !== '' }); + result.shift(); + var swapTotal = 0; + var swapUsed = 0; + for (var line of result) { + line = line.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; + } + 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 || constants.NO_REPORT_GENERATED + 'Network'); + }); +} + +module.exports = WinStats; + diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..ddc5e81 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,208 @@ +var exec = require('child_process').exec; +var execSync = require('child_process').execSync; +var os = require('os'); +var fs = require('fs'); + +var proxyAuthToBase64 = function (proxyObj) { + if (typeof proxyObj === 'object') { + var base64Auth = Buffer.from(proxyObj.username + ":" + proxyObj.password); + } else if (typeof proxyObj === 'string') { + var base64Auth = Buffer.from(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 + */ +var fetchPropertyValue = function (content, propertyToFetch, separator) { + separator = separator || ':'; + propertyToFetch = propertyToFetch.toLowerCase(); + for (var line of content) { + var modifiedLine = line.toLowerCase().replace(/\t/g, ''); + if (modifiedLine.startsWith(propertyToFetch)) { + var splitModifiedLine = modifiedLine.split(separator); + if (modifiedLine.length >= 2) { + splitModifiedLine.shift(); + return splitModifiedLine.join(separator).trim(); + } else { + return ''; + } + } + } + return ''; +} + +var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine) { + line = safeToString(line) || 'null/undefined'; + 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); + } else { + line = line; + } + + return newLine ? line + os.EOL : line; + } +} + +/** + * + * @param {String} content + * @param {String} title + * @param {Date} generatedAt + * @param {Date} startTime + */ +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() + + formatAndBeautifyLine("=", "*", "*", 90, true); + + return content; +} + +var execMultiple = function (commands, callback) { + if (!Array.isArray(commands)) { + return "COMMANDS_IS_NOT_AN_ARRAY"; + } + + var resultArray = new Array(commands.length); + var totalCommandsCompleted = 0; + + commands.forEach(function (cmd, index) { + 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) { + callback(resultArray); + } + }) + }) +} + +var getWmicPath = function () { + if (os.type() === 'Windows_NT') { + var wmicPath = process.env.WINDIR + '\\system32\\wbem\\wmic.exe'; + if (!fs.existsSync(wmicPath)) { + try { + var whereWmicArray = execSync('WHERE WMIC').toString().split('\r\n'); + if (whereWmicArray && whereWmicArray[0]) { + wmicPath = whereWmicArray[0]; + } else { + wmicPath = 'wmic'; + } + } catch (e) { + wmicPath = 'wmic'; + } + } + } + return wmicPath + ' '; +} + +var beautifyObject = function (obj, keysTitle, valuesTitle, maxKeyLength, maxValLength) { + if (typeof obj !== 'object') return 'No Object Passed' + os.EOL; + if (Array.isArray(obj)) { + var longestKeyOfAll = 0; + var longestValOfAll = 0; + for (var indObj of obj) { + if (typeof indObj !== 'object' && !Array.isArray(indObj)) continue; + var indObjKeyLength = getLongestVal(Object.keys(indObj)); + var indObjValLength = getLongestVal(Object.keys(indObj).map(function (key) { return indObj[key] })); + longestKeyOfAll = indObjKeyLength > longestKeyOfAll ? indObjKeyLength : longestKeyOfAll; + longestValOfAll = indObjValLength > longestValOfAll ? indObjValLength : longestValOfAll; + } + + var aggResult = ''; + for (var indObj of obj) { + aggResult += beautifyObject(indObj, 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("-", "-", "", 90, true); + + Object.keys(obj).forEach(function (key) { + finalResult += formatAndBeautifyLine(key, " ", " ", longestKey, false) + + ' : ' + + formatAndBeautifyLine(obj[key], " ", " ", longestVal, true); + }); + + return os.EOL + finalResult + os.EOL; +} + +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; +} + +var safeToString = function (val) { + try { + val = val.toString(); + } catch (e) { + val = JSON.stringify(val); + } + return val; +} + +var isValidCallback = function (checkCallback) { + return (checkCallback && typeof checkCallback === 'function'); +} + +module.exports = { + proxyAuthToBase64, + fetchPropertyValue, + formatAndBeautifyLine, + generateHeaderAndFooter, + execMultiple, + getWmicPath, + beautifyObject, + isValidCallback +} \ No newline at end of file From 4f3645b24f0365b40f278ffda903644f83391091 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Fri, 29 May 2020 21:24:07 +0530 Subject: [PATCH 02/54] unit test setup using mocha, chai and sinon. utils half covered. Utils fixes. --- package.json | 14 +++- src/utils.js | 18 ++--- test/utils.test.js | 194 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 test/utils.test.js diff --git a/package.json b/package.json index 3ebc300..292fb1f 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,24 @@ "description": "Tool to debug failed/dropped requests at client side", "main": "src/node.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "nyc --reporter=html ./node_modules/mocha/bin/mocha test/**/*.test.js" }, "author": "", "license": "ISC", "devDependencies": { - "pkg": "^4.4.8" + "chai": "^4.2.0", + "mocha": "^5.2.0", + "nyc": "^11.9.0", + "pkg": "^4.4.8", + "sinon": "^7.5.0" }, "dependencies": { "winston": "2.4.4" + }, + "nyc": { + "exclude": [ + "**/*.test.js", + "node_modules" + ] } } diff --git a/src/utils.js b/src/utils.js index ddc5e81..79d5d4e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,4 @@ -var exec = require('child_process').exec; -var execSync = require('child_process').execSync; +var cp = require('child_process'); var os = require('os'); var fs = require('fs'); @@ -25,7 +24,7 @@ var fetchPropertyValue = function (content, propertyToFetch, separator) { var modifiedLine = line.toLowerCase().replace(/\t/g, ''); if (modifiedLine.startsWith(propertyToFetch)) { var splitModifiedLine = modifiedLine.split(separator); - if (modifiedLine.length >= 2) { + if (splitModifiedLine.length >= 2) { splitModifiedLine.shift(); return splitModifiedLine.join(separator).trim(); } else { @@ -53,8 +52,6 @@ var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine line = prefix.toString().repeat(remainingCharacters) + " " + line; } else if (suffix) { line = line + " " + suffix.toString().repeat(remainingCharacters); - } else { - line = line; } return newLine ? line + os.EOL : line; @@ -98,7 +95,7 @@ var execMultiple = function (commands, callback) { var totalCommandsCompleted = 0; commands.forEach(function (cmd, index) { - exec(cmd, function (err, result) { + cp.exec(cmd, function (err, result) { if (!err) { resultArray[index] = { content: result, generatedAt: new Date() } } else { @@ -117,7 +114,7 @@ var getWmicPath = function () { var wmicPath = process.env.WINDIR + '\\system32\\wbem\\wmic.exe'; if (!fs.existsSync(wmicPath)) { try { - var whereWmicArray = execSync('WHERE WMIC').toString().split('\r\n'); + var whereWmicArray = cp.execSync('WHERE WMIC').toString().split('\r\n'); if (whereWmicArray && whereWmicArray[0]) { wmicPath = whereWmicArray[0]; } else { @@ -127,8 +124,10 @@ var getWmicPath = function () { wmicPath = 'wmic'; } } + return wmicPath + ' '; + } else { + throw Error('Not Windows Platform'); } - return wmicPath + ' '; } var beautifyObject = function (obj, keysTitle, valuesTitle, maxKeyLength, maxValLength) { @@ -204,5 +203,6 @@ module.exports = { execMultiple, getWmicPath, beautifyObject, - isValidCallback + isValidCallback, + safeToString } \ No newline at end of file diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..8ae3d4c --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,194 @@ + +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 assert = chai.assert; +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 = Buffer.from(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 = Buffer.from(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 Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '=', 60, false); + expect(beautifiedLine).to.eql('============ Hello, This is Network Utility Tool ============='); + }); + + it('should prefix the line with the given values and make it equal to the given length', function () { + var lineToBeautify = "Hello, This is Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '', 60, false); + expect(beautifiedLine).to.eql('========================= Hello, This is Network Utility Tool'); + }); + + it('should suffix the line with the given values and make it equal to the given length', function () { + var lineToBeautify = "Hello, This is Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 60, false); + expect(beautifiedLine).to.eql('Hello, This is Network Utility 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 Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '=', 60, true); + expect(beautifiedLine).to.eql('============ Hello, This is Network Utility 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 Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '=', '', 60, true); + expect(beautifiedLine).to.eql('========================= Hello, This is Network Utility 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 Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 60, true); + expect(beautifiedLine).to.eql('Hello, This is Network Utility 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 Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 10, false); + expect(beautifiedLine).to.eql('Hello, This is Network Utility 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 Network Utility Tool"; + var beautifiedLine = Utils.formatAndBeautifyLine(lineToBeautify, '', '=', 10, true); + expect(beautifiedLine).to.eql('Hello, This is Network Utility Tool' + os.EOL); + }); + }); + + context('generateHeaderAndFooter', function () { + + }); + + 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'); + }); + }); + + 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 () { + + }); +}); From 7ddf0c1dfb7eadac3ed252f1fd058e25b523a253 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Fri, 29 May 2020 21:43:51 +0530 Subject: [PATCH 03/54] more tests for utils --- src/utils.js | 4 ++-- test/utils.test.js | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index 79d5d4e..b6ac451 100644 --- a/src/utils.js +++ b/src/utils.js @@ -88,7 +88,7 @@ var generateHeaderAndFooter = function (content, title, generatedAt, startTime) var execMultiple = function (commands, callback) { if (!Array.isArray(commands)) { - return "COMMANDS_IS_NOT_AN_ARRAY"; + throw Error("COMMANDS_IS_NOT_AN_ARRAY"); } var resultArray = new Array(commands.length); @@ -103,7 +103,7 @@ var execMultiple = function (commands, callback) { } if (++totalCommandsCompleted === commands.length) { - callback(resultArray); + if (isValidCallback(callback)) callback(resultArray); } }) }) diff --git a/test/utils.test.js b/test/utils.test.js index 8ae3d4c..549e1f8 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -189,6 +189,31 @@ describe('Utils', function () { }); 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(); + }); }); }); From f8fa072f11053bca51e521a7344a298e5453d834 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Sat, 30 May 2020 17:43:13 +0530 Subject: [PATCH 04/54] spec for mac stats partial. --- package.json | 2 +- src/stats/macStats.js | 6 +-- src/utils.js | 6 +-- test/stats/baseStats.test.js | 24 ++++++++++ test/stats/macStats.test.js | 88 ++++++++++++++++++++++++++++++++++++ test/utils.test.js | 7 ++- 6 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 test/stats/baseStats.test.js create mode 100644 test/stats/macStats.test.js diff --git a/package.json b/package.json index 292fb1f..94f3dea 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Tool to debug failed/dropped requests at client side", "main": "src/node.js", "scripts": { - "test": "nyc --reporter=html ./node_modules/mocha/bin/mocha test/**/*.test.js" + "test": "nyc --reporter=html ./node_modules/mocha/bin/mocha 'test/**/*.test.js'" }, "author": "", "license": "ISC", diff --git a/src/stats/macStats.js b/src/stats/macStats.js index 3b8b303..6dc4507 100644 --- a/src/stats/macStats.js +++ b/src/stats/macStats.js @@ -1,6 +1,6 @@ var os = require('os'); var BaseStats = require('./baseStats'); -var exec = require('child_process').exec; +var cp = require('child_process'); var Utils = require('../utils'); var constants = require('../../config/constants'); @@ -9,7 +9,7 @@ MacStats.description = "System and Network Stats for Mac"; MacStats.cpu = function (callback) { var startTime = new Date(); - exec(constants.MAC.TOP_3_SAMPLES, function (err, result) { + 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); @@ -30,7 +30,7 @@ MacStats.mem = function (callback) { memStats.used = memStats.total - memStats.free; - exec(constants.MAC.SWAP_USAGE, function (err, result) { + cp.exec(constants.MAC.SWAP_USAGE, function (err, result) { if (!err) { var resultLines = result.toString().split('\n'); if (resultLines[0]) { diff --git a/src/utils.js b/src/utils.js index b6ac451..d6125f4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -36,7 +36,7 @@ var fetchPropertyValue = function (content, propertyToFetch, separator) { } var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine) { - line = safeToString(line) || 'null/undefined'; + line = safeToString(line); if (line) { var lineLength = line.length; idealLength = idealLength || 70; @@ -80,7 +80,7 @@ var generateHeaderAndFooter = function (content, title, generatedAt, startTime) + formatAndBeautifyLine("Start Time: " + startTime, "", "=", 90, true) + formatAndBeautifyLine("Generated At: " + generatedAt, "", "=", 90, true) + formatAndBeautifyLine("=", "*", "*", 90, true) - + content.toString() + + content.toString() + os.EOL + formatAndBeautifyLine("=", "*", "*", 90, true); return content; @@ -186,7 +186,7 @@ var safeToString = function (val) { try { val = val.toString(); } catch (e) { - val = JSON.stringify(val); + val = JSON.stringify(val) || 'undefined'; } return val; } diff --git a/test/stats/baseStats.test.js b/test/stats/baseStats.test.js new file mode 100644 index 0000000..449fc7a --- /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'); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/stats/macStats.test.js b/test/stats/macStats.test.js new file mode 100644 index 0000000..cb222dd --- /dev/null +++ b/test/stats/macStats.test.js @@ -0,0 +1,88 @@ +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.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 + } + 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 + } + + MacStats.mem(function (result) { + sinon.assert.calledWith(Utils.beautifyObject, memStats, "Memory", "Bytes"); + }); + + os.totalmem.restore(); + os.freemem.restore(); + cp.exec.restore(); + Utils.beautifyObject.restore(); + }); + }); +}); \ No newline at end of file diff --git a/test/utils.test.js b/test/utils.test.js index 549e1f8..799853b 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -5,7 +5,6 @@ var Utils = require('../src/utils'); var os = require('os'); var fs = require('fs'); var cp = require('child_process'); -var assert = chai.assert; var expect = chai.expect; describe('Utils', function () { @@ -115,6 +114,12 @@ describe('Utils', function () { 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 () { From 536fa9d346cf44749c8357a58937dad0fc3f61dc Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Sat, 30 May 2020 17:49:19 +0530 Subject: [PATCH 05/54] fixed minor and patch versions for dev deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 94f3dea..e5c08fe 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,11 @@ "author": "", "license": "ISC", "devDependencies": { - "chai": "^4.2.0", - "mocha": "^5.2.0", - "nyc": "^11.9.0", - "pkg": "^4.4.8", - "sinon": "^7.5.0" + "chai": "4.2.0", + "mocha": "5.2.0", + "nyc": "11.9.0", + "pkg": "4.4.8", + "sinon": "7.5.0" }, "dependencies": { "winston": "2.4.4" From cb97f785141a3e097fd4df95c3226a93dabcab8b Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Sun, 31 May 2020 00:37:07 +0530 Subject: [PATCH 06/54] mac network stats unit test --- src/stats/macStats.js | 2 +- test/stats/macStats.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/stats/macStats.js b/src/stats/macStats.js index 6dc4507..ec9f05b 100644 --- a/src/stats/macStats.js +++ b/src/stats/macStats.js @@ -67,7 +67,7 @@ MacStats.network = function (callback) { finalOutput = finalOutput + Utils.generateHeaderAndFooter(results[i].content, "Network Stat: '" + commands[i] + "'", results[i].generatedAt, startTime); } - if (Utils.isValidCallback(callback)) callback(finalOutput || constants.NO_REPORT_GENERATED + 'Network'); + if (Utils.isValidCallback(callback)) callback(finalOutput); }); } diff --git a/test/stats/macStats.test.js b/test/stats/macStats.test.js index cb222dd..f11de49 100644 --- a/test/stats/macStats.test.js +++ b/test/stats/macStats.test.js @@ -85,4 +85,29 @@ describe('MacStats', function () { Utils.beautifyObject.restore(); }); }); + + context('Network Stats', function () { + it('callbacks with the stats content of multiple commands beautified', 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(); + }); + }); }); \ No newline at end of file From 99954d33b17e83348d8739e1a6adcc824d6eb9d7 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Mon, 1 Jun 2020 20:01:48 +0530 Subject: [PATCH 07/54] more specs and fixes --- config/constants.js | 7 +- src/commandLine.js | 7 +- src/connectivity.js | 7 +- src/logger.js | 4 +- src/stats/linuxStats.js | 6 +- src/stats/winStats.js | 11 ++-- test/commandLine.test.js | 75 ++++++++++++++++++++++ test/connectivity.test.js | 6 ++ test/logger.test.js | 117 ++++++++++++++++++++++++++++++++++ test/stats/baseStats.test.js | 2 +- test/stats/linuxStats.test.js | 114 +++++++++++++++++++++++++++++++++ test/stats/macStats.test.js | 4 +- test/stats/winStats.test.js | 107 +++++++++++++++++++++++++++++++ 13 files changed, 446 insertions(+), 21 deletions(-) create mode 100644 test/commandLine.test.js create mode 100644 test/connectivity.test.js create mode 100644 test/logger.test.js create mode 100644 test/stats/linuxStats.test.js create mode 100644 test/stats/winStats.test.js diff --git a/config/constants.js b/config/constants.js index d7ba8d2..00b2e15 100644 --- a/config/constants.js +++ b/config/constants.js @@ -3,6 +3,7 @@ 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 = 7000; module.exports.CLIENT_REQ_TIMEOUT = 50000; +module.exports.DEFAULT_PROXY_PORT = '3128'; module.exports.REQ_TIMED_OUT = "Request Timed Out. Did not get any response for " + this.CLIENT_REQ_TIMEOUT + ' ms.'; module.exports.REQ_FAILED_MSG = "Request Failed At Network Tool"; module.exports.LOGS = Object.freeze({ @@ -12,7 +13,11 @@ module.exports.LOGS = Object.freeze({ REQUESTS: 'Requests.log', CONNECTIVITY: 'Connectivity.log' }); -module.exports.NwtGlobalConfig = {}; +module.exports.NwtGlobalConfig = { + deleteProxy: function () { + delete this.proxy; + } +}; module.exports.COMMON = Object.freeze({ PING_HUB: 'ping -c 5 hub-cloud.browserstack.com', PING_AUTOMATE: 'ping -c 5 automate.browserstack.com' diff --git a/src/commandLine.js b/src/commandLine.js index a6efe4f..2e20060 100644 --- a/src/commandLine.js +++ b/src/commandLine.js @@ -1,4 +1,5 @@ -var NwtGlobalConfig = require('../config/constants').NwtGlobalConfig; +var constants = require('../config/constants'); +var NwtGlobalConfig = constants.NwtGlobalConfig; var CommandLineManager = { validArgValue: function (value) { @@ -19,7 +20,7 @@ var CommandLineManager = { NwtGlobalConfig.proxy['port'] = argv[index + 1]; argv.splice(index, 2); } else { - NwtGlobalConfig.proxy['port'] = 3128; + NwtGlobalConfig.proxy['port'] = constants.DEFAULT_PROXY_PORT; } index = argv.indexOf('--proxy-user'); @@ -46,4 +47,4 @@ var CommandLineManager = { } } -module.exports = CommandLineManager; \ No newline at end of file +module.exports = CommandLineManager; diff --git a/src/connectivity.js b/src/connectivity.js index 6eef217..b66836d 100644 --- a/src/connectivity.js +++ b/src/connectivity.js @@ -123,13 +123,14 @@ var ConnectivityChecker = { var parsedUrl = url.parse(reqUrl); var reqOptions = { method: 'GET', - headers: { - 'Proxy-Authorization': Utils.proxyAuthToBase64(NwtGlobalConfig.proxy) - }, + headers: {}, host: NwtGlobalConfig.proxy.host, port: NwtGlobalConfig.proxy.port, path: parsedUrl.href } + if (NwtGlobalConfig.proxy.username && NwtGlobalConfig.proxy.password) { + reqOptions.headers['Proxy-Authorization'] = Utils.proxyAuthToBase64(NwtGlobalConfig.proxy); + } return reqOptions; } } diff --git a/src/logger.js b/src/logger.js index 6f1f6d6..6be887d 100644 --- a/src/logger.js +++ b/src/logger.js @@ -2,9 +2,9 @@ var winston = require('winston'); var LogManager = { getLogger: function (filename) { - return new (winston.Logger)({ + return new winston.Logger({ transports: [ - new (winston.transports.File)({ + new winston.transports.File({ filename }) ] diff --git a/src/stats/linuxStats.js b/src/stats/linuxStats.js index bfe9fbe..dfe8f51 100644 --- a/src/stats/linuxStats.js +++ b/src/stats/linuxStats.js @@ -1,6 +1,6 @@ var os = require('os'); var BaseStats = require('./baseStats'); -var exec = require('child_process').exec; +var cp = require('child_process'); var fs = require('fs'); var Utils = require('../utils'); var constants = require('../../config/constants'); @@ -10,7 +10,7 @@ LinuxStats.description = "System and Network Related stats for Linux"; LinuxStats.cpu = function (callback) { var startTime = new Date(); - exec(constants.LINUX.TOP_3_SAMPLES, function (err, result) { + 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); @@ -60,7 +60,7 @@ LinuxStats.network = function (callback) { finalOutput = finalOutput + Utils.generateHeaderAndFooter(results[i].content, "Network Stat: '" + commands[i] + "'", results[i].generatedAt, startTime); } - if (Utils.isValidCallback(callback)) callback(finalOutput || constants.NO_REPORT_GENERATED + 'Network'); + if (Utils.isValidCallback(callback)) callback(finalOutput); }); } diff --git a/src/stats/winStats.js b/src/stats/winStats.js index 0d379e2..c484e67 100644 --- a/src/stats/winStats.js +++ b/src/stats/winStats.js @@ -1,7 +1,6 @@ var os = require('os'); var BaseStats = require('./baseStats'); -var exec = require('child_process').exec; -var fs = require('fs'); +var cp = require('child_process'); var Utils = require('../utils'); var constants = require('../../config/constants'); @@ -12,9 +11,9 @@ 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 = startTime(); + var startTime = new Date(); - exec(WinStats.wmicPath + constants.WIN.LOAD_PERCENTAGE, function (err, result) { + cp.exec(WinStats.wmicPath + constants.WIN.LOAD_PERCENTAGE, function (err, result) { if (!err) { result = Utils.generateHeaderAndFooter(result, "Load Percentage", new Date(), startTime); } @@ -36,7 +35,7 @@ WinStats.mem = function (callback) { WinStats.wmicPath = WinStats.wmicPath || Utils.getWmicPath(); - exec(WinStats.wmicPath + constants.WIN.SWAP_USAGE, function (err, result) { + cp.exec(WinStats.wmicPath + constants.WIN.SWAP_USAGE, function (err, result) { if (!err) { result = result.split('\r\n').filter(function (line) { return line.trim() !== '' }); result.shift(); @@ -65,7 +64,7 @@ WinStats.network = function (callback) { finalOutput = finalOutput + Utils.generateHeaderAndFooter(results[i].content, "Network Stat: '" + commands[i] + "'", results[i].generatedAt, startTime); } - if (Utils.isValidCallback(callback)) callback(finalOutput || constants.NO_REPORT_GENERATED + 'Network'); + if (Utils.isValidCallback(callback)) callback(finalOutput); }); } diff --git a/test/commandLine.test.js b/test/commandLine.test.js new file mode 100644 index 0000000..c63bcb1 --- /dev/null +++ b/test/commandLine.test.js @@ -0,0 +1,75 @@ +var CommandLineManager = require('../src/commandLine'); +var constants = require('../config/constants'); +var NwtGlobalConfig = constants.NwtGlobalConfig; +var expect = require('chai').expect; + +describe('CommandLineManager', function () { + context('processArgs', function () { + context('Proxy Arguments', function () { + beforeEach(function () { + NwtGlobalConfig.deleteProxy(); + }); + + it('parse proxy-host and proxy-port params', function () { + var argv = ['--proxy-host', 'host', '--proxy-port', '9687']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.proxy.host).to.eql('host'); + expect(NwtGlobalConfig.proxy.port).to.eql('9687'); + }); + + it('parse proxy-host, proxy-port, proxy-user and proxy-pass', function () { + var argv = ['--proxy-host', 'host', '--proxy-port', '9687', '--proxy-user', 'user', '--proxy-pass', 'pass']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.proxy.host).to.eql('host'); + expect(NwtGlobalConfig.proxy.port).to.eql('9687'); + expect(NwtGlobalConfig.proxy.username).to.eql('user'); + expect(NwtGlobalConfig.proxy.password).to.eql('pass'); + }); + + it('default proxy port if only proxy host is provided', function () { + var argv = ['--proxy-host', 'host']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.proxy.host).to.eql('host'); + expect(NwtGlobalConfig.proxy.port).to.eql(constants.DEFAULT_PROXY_PORT); + }); + + it('empty proxy password if only proxy username is provided', function () { + var argv = ['--proxy-host', 'host', '--proxy-port', '9687', '--proxy-user', 'user']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.proxy.host).to.eql('host'); + expect(NwtGlobalConfig.proxy.port).to.eql('9687'); + expect(NwtGlobalConfig.proxy.username).to.eql('user'); + expect(NwtGlobalConfig.proxy.password).to.eql(''); + }); + + it("proxy won't be set if proxy host is not provided", function () { + var argv = ['--proxy-port', '9687']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.proxy).to.eql(undefined); + }); + + it("proxy auth won't be set if proxy username is not provided", function () { + var argv = ['--proxy-host', 'host', '--proxy-port', '9687', '--proxy-pass', 'pass']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.proxy.host).to.eql('host'); + expect(NwtGlobalConfig.proxy.port).to.eql('9687'); + expect(NwtGlobalConfig.proxy.username).to.eql(undefined); + expect(NwtGlobalConfig.proxy.password).to.eql(undefined); + }); + }); + + context('Delete Existing Logs Argument', function () { + it('defaults to no deletion of existing logs if argument is not provided', function () { + var argv = []; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.deleteExistingLogs).to.be.false; + }); + + it('set to true if argument if provided, i.e. existing logs will be deleted', function () { + var argv = ['--del-logs']; + CommandLineManager.processArgs(argv); + expect(NwtGlobalConfig.deleteExistingLogs).to.be.true; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/connectivity.test.js b/test/connectivity.test.js new file mode 100644 index 0000000..e713e40 --- /dev/null +++ b/test/connectivity.test.js @@ -0,0 +1,6 @@ +var http = require('http'); +var https = require('https'); +var ConnectivityChecker = require('../src/connectivity'); +var constants = require('../config/constants'); +var NwtGlobalConfig = constants.NwtGlobalConfig; +var Utils = require('../src/utils'); diff --git a/test/logger.test.js b/test/logger.test.js new file mode 100644 index 0000000..3da0946 --- /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(); + }); + }); +}); \ No newline at end of file diff --git a/test/stats/baseStats.test.js b/test/stats/baseStats.test.js index 449fc7a..4304369 100644 --- a/test/stats/baseStats.test.js +++ b/test/stats/baseStats.test.js @@ -21,4 +21,4 @@ describe('BaseStats', function () { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/stats/linuxStats.test.js b/test/stats/linuxStats.test.js new file mode 100644 index 0000000..cff801a --- /dev/null +++ b/test/stats/linuxStats.test.js @@ -0,0 +1,114 @@ +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.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 + } + 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 + } + + 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 index f11de49..82e8cbc 100644 --- a/test/stats/macStats.test.js +++ b/test/stats/macStats.test.js @@ -87,7 +87,7 @@ describe('MacStats', function () { }); context('Network Stats', function () { - it('callbacks with the stats content of multiple commands beautified', function () { + it('callbacks with the stats content of multiple commands', function () { var results = [{ content: 'resultOne', generatedAt: new Date().toISOString() @@ -110,4 +110,4 @@ describe('MacStats', function () { Utils.execMultiple.restore(); }); }); -}); \ No newline at end of file +}); diff --git a/test/stats/winStats.test.js b/test/stats/winStats.test.js new file mode 100644 index 0000000..b1a831f --- /dev/null +++ b/test/stats/winStats.test.js @@ -0,0 +1,107 @@ +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.NO_REPORT_GENERATED + 'CPU' + os.EOL); + }); + + cp.exec.restore(); + }); + }); + + context('Mem Stats', function () { + it('callbacks with result of mem stats', function () { + + }); + + 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 + } + + 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(); + }); + }); +}); From a93ffdef9d736a25017ad26a2a1fed721d8b4792 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Mon, 1 Jun 2020 21:48:04 +0530 Subject: [PATCH 08/54] nock for mocking http requests. connectivity check without proxy tests. --- config/constants.js | 6 + package-lock.json | 301 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + test/connectivity.test.js | 78 +++++++++- 4 files changed, 384 insertions(+), 2 deletions(-) diff --git a/config/constants.js b/config/constants.js index 00b2e15..97937bd 100644 --- a/config/constants.js +++ b/config/constants.js @@ -16,6 +16,12 @@ module.exports.LOGS = Object.freeze({ module.exports.NwtGlobalConfig = { deleteProxy: function () { delete this.proxy; + }, + initializeDummyLoggers: function () { + this.ConnLogger = { + info: function () {}, + error: function () {} + } } }; module.exports.COMMON = Object.freeze({ diff --git a/package-lock.json b/package-lock.json index 21b5036..a0e8309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "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 + }, "async": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", @@ -147,6 +153,20 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "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", @@ -157,6 +177,12 @@ "supports-color": "^7.1.0" } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -206,12 +232,53 @@ "assert-plus": "^1.0.0" } }, + "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" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -237,6 +304,36 @@ "safer-buffer": "^2.1.0" } }, + "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" + } + }, "escodegen": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", @@ -379,6 +476,18 @@ "universalify": "^0.1.0" } }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "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 + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -433,12 +542,27 @@ "har-schema": "^2.0.0" } }, + "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 + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -472,6 +596,24 @@ "p-is-promise": "^3.0.0" } }, + "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", @@ -493,6 +635,24 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "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" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -565,6 +725,12 @@ "type-check": "~0.3.2" } }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "merge2": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", @@ -611,6 +777,12 @@ "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 + }, "multistream": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", @@ -621,12 +793,71 @@ "readable-stream": "^2.0.5" } }, + "nock": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz", + "integrity": "sha512-EDgl/WgNQ0C1BZZlASOQkQdE6tAWXJi8QQlugqzN64JJkvZ7ILijZuG24r4vCC7yOfnm6HKpne5AGExLGCeBWg==", + "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": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "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" + } + }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -665,6 +896,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -735,6 +972,12 @@ "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 + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -774,6 +1017,16 @@ "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", "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" + } + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -894,6 +1147,48 @@ "readable-stream": "^2.1.4" } }, + "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" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -961,6 +1256,12 @@ "prelude-ls": "~1.1.2" } }, + "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 + }, "uid2": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", diff --git a/package.json b/package.json index e5c08fe..0d29367 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "devDependencies": { "chai": "4.2.0", "mocha": "5.2.0", + "nock": "9.6.1", "nyc": "11.9.0", "pkg": "4.4.8", "sinon": "7.5.0" diff --git a/test/connectivity.test.js b/test/connectivity.test.js index e713e40..63426c0 100644 --- a/test/connectivity.test.js +++ b/test/connectivity.test.js @@ -1,6 +1,80 @@ -var http = require('http'); -var https = require('https'); +var url = require('url'); var ConnectivityChecker = require('../src/connectivity'); var constants = require('../config/constants'); var NwtGlobalConfig = constants.NwtGlobalConfig; var Utils = require('../src/utils'); +var nock = require('nock'); +var sinon = require('sinon'); + +function getRequestMocker(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); + return nock(type + '://' + parsedUrl.hostname) + .get(parsedUrl.path) + .reply(statusCode, data); +} + +describe('Connectivity Checker for BrowserStack Components', function () { + context('without Proxy', function () { + beforeEach(function () { + getRequestMocker(constants.HUB_STATUS_URL, 'http', null, 200); + getRequestMocker(constants.HUB_STATUS_URL, 'https', null, 200); + getRequestMocker(constants.RAILS_AUTOMATE, 'http', null, 301); + getRequestMocker(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'); + NwtGlobalConfig.deleteProxy(); + NwtGlobalConfig.initializeDummyLoggers(); + + var result = [{ + 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' + } + ]; + + ConnectivityChecker.fireChecks("some topic", 1, function () { + sinon.assert.calledOnceWithExactly(Utils.beautifyObject, result, "Result Key", "Result Value"); + Utils.beautifyObject.restore(); + done(); + }); + }); + }); +}); From 4cb990ff6d675a04c580e6137ea11f75d8a922cb Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 00:46:50 +0530 Subject: [PATCH 09/54] connectivity checks specs with proxy cases and error case --- config/constants.js | 14 +++ test/connectivity.test.js | 180 +++++++++++++++++++++++++++++++++++--- 2 files changed, 181 insertions(+), 13 deletions(-) diff --git a/config/constants.js b/config/constants.js index 97937bd..0644c41 100644 --- a/config/constants.js +++ b/config/constants.js @@ -14,14 +14,28 @@ module.exports.LOGS = Object.freeze({ CONNECTIVITY: 'Connectivity.log' }); module.exports.NwtGlobalConfig = { + initializeDummyProxy: function () { + this.proxy = { + host: "dummyhost12345.com", + port: "3128", + username: "user", + password: "pass" + } + }, + deleteProxy: function () { delete this.proxy; }, + initializeDummyLoggers: function () { this.ConnLogger = { info: function () {}, error: function () {} } + }, + + deleteLoggers: function () { + delete this.ConnLogger; } }; module.exports.COMMON = Object.freeze({ diff --git a/test/connectivity.test.js b/test/connectivity.test.js index 63426c0..c77106e 100644 --- a/test/connectivity.test.js +++ b/test/connectivity.test.js @@ -6,7 +6,7 @@ var Utils = require('../src/utils'); var nock = require('nock'); var sinon = require('sinon'); -function getRequestMocker(reqUrl, type, data, statusCode) { +function nockGetRequest(reqUrl, type, data, statusCode) { data = (data && typeof data === 'object') ? data : { "data": "value" }; type = (['http', 'https'].indexOf(type) !== -1) ? type : 'http'; try { @@ -16,29 +16,117 @@ function getRequestMocker(reqUrl, type, data, statusCode) { } var parsedUrl = url.parse(reqUrl); - return nock(type + '://' + parsedUrl.hostname) + var port = parsedUrl.port; + port = port || (type === 'http' ? '80' : '443'); + return nock(type + '://' + parsedUrl.hostname + ':' + port) .get(parsedUrl.path) .reply(statusCode, 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); +} + +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'); +} + describe('Connectivity Checker for BrowserStack Components', function () { context('without Proxy', function () { beforeEach(function () { - getRequestMocker(constants.HUB_STATUS_URL, 'http', null, 200); - getRequestMocker(constants.HUB_STATUS_URL, 'https', null, 200); - getRequestMocker(constants.RAILS_AUTOMATE, 'http', null, 301); - getRequestMocker(constants.RAILS_AUTOMATE, 'https', null, 302); + NwtGlobalConfig.deleteProxy(); + NwtGlobalConfig.initializeDummyLoggers(); + ConnectivityChecker.connectionChecks = []; + nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); + nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); + nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); }); afterEach(function () { nock.cleanAll(); + NwtGlobalConfig.deleteLoggers(); }); it('HTTP(S) to Hub & Rails', function (done) { this.timeout(2000); sinon.stub(Utils, 'beautifyObject'); - NwtGlobalConfig.deleteProxy(); + + var result = [{ + 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' + } + ]; + + ConnectivityChecker.fireChecks("some topic", 1, function () { + sinon.assert.calledOnceWithExactly(Utils.beautifyObject, result, "Result Key", "Result Value"); + Utils.beautifyObject.restore(); + done(); + }); + }); + }); + + context('with Proxy', function () { + beforeEach(function () { + NwtGlobalConfig.initializeDummyProxy(); NwtGlobalConfig.initializeDummyLoggers(); + ConnectivityChecker.connectionChecks = []; + nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); + nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); + nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); + nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'hub', null, 200); + nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'automate', null, 301); + }); + + afterEach(function () { + nock.cleanAll(); + NwtGlobalConfig.deleteLoggers(); + NwtGlobalConfig.deleteProxy(); + }); + + it('HTTP(S) to Hub & Rails', function (done) { + this.timeout(2000); + sinon.stub(Utils, 'beautifyObject'); var result = [{ data: '{"data":"value"}', @@ -46,27 +134,93 @@ describe('Connectivity Checker for BrowserStack Components', function () { 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' + }, { + 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, result, "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 () { + NwtGlobalConfig.deleteProxy(); + NwtGlobalConfig.initializeDummyLoggers(); + ConnectivityChecker.connectionChecks = []; + nockGetRequestWithError(constants.HUB_STATUS_URL, 'http'); + nockGetRequestWithError(constants.HUB_STATUS_URL, 'https'); + nockGetRequestWithError(constants.RAILS_AUTOMATE, 'http'); + nockGetRequestWithError(constants.RAILS_AUTOMATE, 'https'); + }); + + afterEach(function () { + nock.cleanAll(); + NwtGlobalConfig.deleteLoggers(); + }); + + it('HTTP(S) to Hub & Rails', function (done) { + this.timeout(2000); + sinon.stub(Utils, 'beautifyObject'); + + var result = [{ + 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' } ]; From a4ba616aca54cb4327d0e358809ba248bc3de40c Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 14:48:26 +0530 Subject: [PATCH 10/54] specs for server done. win mem stats specs remain. dummy initializers for loggers, handlers etc for testing. specs for stats covered --- config/constants.js | 36 ++++++++++ package.json | 7 +- src/node.js | 13 +++- src/server.js | 34 +++++++-- src/stats/baseStats.js | 2 +- src/stats/linuxStats.js | 2 +- src/utils.js | 2 +- test/connectivity.test.js | 73 ++++--------------- test/helper.js | 51 ++++++++++++++ test/server.test.js | 120 ++++++++++++++++++++++++++++++++ test/stats/statsFactory.test.js | 26 +++++++ test/utils.test.js | 83 ++++++++++++++++++++++ 12 files changed, 378 insertions(+), 71 deletions(-) create mode 100644 test/helper.js create mode 100644 test/server.test.js create mode 100644 test/stats/statsFactory.test.js diff --git a/config/constants.js b/config/constants.js index 0644c41..9f2cf47 100644 --- a/config/constants.js +++ b/config/constants.js @@ -1,6 +1,8 @@ module.exports.NO_REPORT_GENERATED = "COULD NOT GENERATE REPORT FOR : "; module.exports.HUB_STATUS_URL = 'http://hub-cloud.browserstack.com/wd/hub/status'; module.exports.RAILS_AUTOMATE = 'http://automate.browserstack.com'; +module.exports.NWT_HANDLER_PORT = 9687; +module.exports.NWT_HANDLER_PORT_TEST = 8787; module.exports.CONNECTIVITY_REQ_TIMEOUT = 7000; module.exports.CLIENT_REQ_TIMEOUT = 50000; module.exports.DEFAULT_PROXY_PORT = '3128'; @@ -31,11 +33,45 @@ module.exports.NwtGlobalConfig = { this.ConnLogger = { info: function () {}, error: function () {} + }, + this.NetworkLogger = { + info: function () {}, + error: function () {} + }, + this.MemLogger = { + info: function () {}, + error: function () {} + }, + this.CPULogger = { + info: function () {}, + error: function () {} + }, + this.ReqLogger = { + info: function () {}, + error: function () {} } }, deleteLoggers: function () { delete this.ConnLogger; + delete this.NetworkLogger; + delete this.MemLogger; + delete this.CPULogger; + delete this.ReqLogger; + }, + + initializeDummyHandlers: function () { + this.NetworkLogHandler = function () {}; + this.ConnHandler = function () {}; + this.CpuLogHandler = function () {}; + this.MemLogHandler = function () {}; + }, + + deleteHandlers: function () { + delete this.NetworkLogHandler; + delete this.ConnHandler; + delete this.CpuLogHandler; + delete this.MemLogHandler; } }; module.exports.COMMON = Object.freeze({ diff --git a/package.json b/package.json index 0d29367..399f5af 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,14 @@ "winston": "2.4.4" }, "nyc": { + "all": true, "exclude": [ "**/*.test.js", - "node_modules" + "test/helper.js", + "node_modules", + "coverage", + "NWT_Logs", + "config" ] } } diff --git a/src/node.js b/src/node.js index fc52179..b4b1e95 100644 --- a/src/node.js +++ b/src/node.js @@ -1,5 +1,6 @@ -var LogFiles = require('../config/constants').LOGS; -var NwtGlobalConfig = require('../config/constants').NwtGlobalConfig; +var constants = require('../config/constants'); +var LogFiles = constants.LOGS; +var NwtGlobalConfig = constants.NwtGlobalConfig; var CommandLineManager = require('./commandLine'); var ConnectivityChecker = require('./connectivity'); var NWTHandler = require('./server'); @@ -72,7 +73,13 @@ var NwTool = { NwtGlobalConfig.NetworkLogHandler("Initial Network"); NwtGlobalConfig.MemLogHandler("Initial Memory"); NwtGlobalConfig.ConnHandler("Initial Connectivity", null, function () { - NWTHandler.startProxy(); + NWTHandler.startProxy(constants.NWT_HANDLER_PORT, function (err, result) { + if (err) { + console.log("Error in starting Network Tool Utility Proxy: ", err); + console.log("Exiting the Tool..."); + process.exit(1); + } + }); }); } } diff --git a/src/server.js b/src/server.js index 1e7f1a4..b38530a 100644 --- a/src/server.js +++ b/src/server.js @@ -199,12 +199,34 @@ var NWTHandler = { }, - startProxy: function () { - NWTHandler.generatorForRequestOptionsObject(); - var server = http.createServer(NWTHandler.requestHandler); - server.listen(9687, function () { - console.log("Started on 9687"); - }); + startProxy: function (port, callback) { + try { + NWTHandler.generatorForRequestOptionsObject(); + NWTHandler.server = http.createServer(NWTHandler.requestHandler); + NWTHandler.server.listen(port); + NWTHandler.server.on('listening', function () { + console.log("Network Utility Tool Proxy Started on Port: ", port); + callback(null, port); + }); + NWTHandler.server.on('error', function (err) { + callback(err.toString(), null); + }) + } catch (e) { + callback(e.toString(), null); + } + }, + + stopProxy: function (callback) { + try { + if (NWTHandler.server) { + NWTHandler.server.close(); + NWTHandler.server = null; + console.log("Network Utility Tool Stopped"); + } + callback(null, true); + } catch (e) { + callback(e.toString(), null); + } } } diff --git a/src/stats/baseStats.js b/src/stats/baseStats.js index ed09530..9542353 100644 --- a/src/stats/baseStats.js +++ b/src/stats/baseStats.js @@ -1,7 +1,7 @@ var Utils = require('../utils'); var BaseStats = { - description: "Base Object for System & Network Stats.", + description: "Base Object for System & Network Stats", cpu: function (callback) { if (Utils.isValidCallback(callback)) callback("CPU Stats Not Yet Implemented"); diff --git a/src/stats/linuxStats.js b/src/stats/linuxStats.js index dfe8f51..4d4630f 100644 --- a/src/stats/linuxStats.js +++ b/src/stats/linuxStats.js @@ -6,7 +6,7 @@ var Utils = require('../utils'); var constants = require('../../config/constants'); var LinuxStats = Object.create(BaseStats); -LinuxStats.description = "System and Network Related stats for Linux"; +LinuxStats.description = "System and Network Stats for Linux"; LinuxStats.cpu = function (callback) { var startTime = new Date(); diff --git a/src/utils.js b/src/utils.js index d6125f4..77ace1d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -131,7 +131,7 @@ var getWmicPath = function () { } var beautifyObject = function (obj, keysTitle, valuesTitle, maxKeyLength, maxValLength) { - if (typeof obj !== 'object') return 'No Object Passed' + os.EOL; + if (typeof obj !== 'object') return 'Not an Object' + os.EOL; if (Array.isArray(obj)) { var longestKeyOfAll = 0; var longestValOfAll = 0; diff --git a/test/connectivity.test.js b/test/connectivity.test.js index c77106e..b8c5588 100644 --- a/test/connectivity.test.js +++ b/test/connectivity.test.js @@ -1,53 +1,10 @@ -var url = require('url'); var ConnectivityChecker = require('../src/connectivity'); var constants = require('../config/constants'); var NwtGlobalConfig = constants.NwtGlobalConfig; var Utils = require('../src/utils'); var nock = require('nock'); var sinon = require('sinon'); - -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); -} - -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); -} - -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'); -} +var helper = require('./helper'); describe('Connectivity Checker for BrowserStack Components', function () { context('without Proxy', function () { @@ -55,10 +12,10 @@ describe('Connectivity Checker for BrowserStack Components', function () { NwtGlobalConfig.deleteProxy(); NwtGlobalConfig.initializeDummyLoggers(); ConnectivityChecker.connectionChecks = []; - nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); - nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); - nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); - nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); + helper.nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + helper.nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); + helper.nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); + helper.nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); }); afterEach(function () { @@ -110,12 +67,12 @@ describe('Connectivity Checker for BrowserStack Components', function () { NwtGlobalConfig.initializeDummyProxy(); NwtGlobalConfig.initializeDummyLoggers(); ConnectivityChecker.connectionChecks = []; - nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); - nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); - nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); - nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); - nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'hub', null, 200); - nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'automate', null, 301); + helper.nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + helper.nockGetRequest(constants.HUB_STATUS_URL, 'https', null, 200); + helper.nockGetRequest(constants.RAILS_AUTOMATE, 'http', null, 301); + helper.nockGetRequest(constants.RAILS_AUTOMATE, 'https', null, 302); + helper.nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'hub', null, 200); + helper.nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'automate', null, 301); }); afterEach(function () { @@ -182,10 +139,10 @@ describe('Connectivity Checker for BrowserStack Components', function () { NwtGlobalConfig.deleteProxy(); NwtGlobalConfig.initializeDummyLoggers(); ConnectivityChecker.connectionChecks = []; - nockGetRequestWithError(constants.HUB_STATUS_URL, 'http'); - nockGetRequestWithError(constants.HUB_STATUS_URL, 'https'); - nockGetRequestWithError(constants.RAILS_AUTOMATE, 'http'); - nockGetRequestWithError(constants.RAILS_AUTOMATE, 'https'); + helper.nockGetRequestWithError(constants.HUB_STATUS_URL, 'http'); + helper.nockGetRequestWithError(constants.HUB_STATUS_URL, 'https'); + helper.nockGetRequestWithError(constants.RAILS_AUTOMATE, 'http'); + helper.nockGetRequestWithError(constants.RAILS_AUTOMATE, 'https'); }); afterEach(function () { diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 0000000..f4a54db --- /dev/null +++ b/test/helper.js @@ -0,0 +1,51 @@ +var nock = require('nock'); +var url = require('url'); + +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); +} + +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); +} + +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'); +} + +module.exports = { + nockGetRequest, + nockProxyUrl, + nockGetRequestWithError +} \ No newline at end of file diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..681ef44 --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,120 @@ +var url = require('url'); +var constants = require('../config/constants'); +var NwtGlobalConfig = constants.NwtGlobalConfig; +var Utils = require('../src/utils'); +var nock = require('nock'); +var sinon = require('sinon'); +var NWTHandler = require('../src/server'); +var http = require('http'); +var assert = require('chai').assert; +var helper = require('./helper'); + +describe('NWTHandler', function () { + context('Proxy Server', function () { + + before(function (done) { + this.timeout = 5000; + helper.nockGetRequest(constants.HUB_STATUS_URL, 'http', null, 200); + NwtGlobalConfig.initializeDummyLoggers(); + NwtGlobalConfig.initializeDummyHandlers(); + + NWTHandler.startProxy(constants.NWT_HANDLER_PORT_TEST, function (port) { + done(); + }); + }); + + after(function (done) { + this.timeout = 5000; + NWTHandler.stopProxy(function () { + done(); + }); + NwtGlobalConfig.deleteLoggers(); + NwtGlobalConfig.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: constants.NWT_HANDLER_PORT_TEST, + 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; + NwtGlobalConfig.initializeDummyProxy(); + helper.nockProxyUrl(NwtGlobalConfig.proxy, 'http', 'hub', null, 200); + NWTHandler.generatorForRequestOptionsObject(); + var reqOptions = { + method: 'GET', + host: 'localhost', + port: constants.NWT_HANDLER_PORT_TEST, + 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(); + NwtGlobalConfig.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; + helper.nockGetRequestWithError(constants.HUB_STATUS_URL, 'http'); + NWTHandler.generatorForRequestOptionsObject(); + var reqOptions = { + method: 'GET', + host: 'localhost', + port: constants.NWT_HANDLER_PORT_TEST, + 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 Network Tool","error":"Request Failed At Network Tool"}'); + done(); + }); + }); + + request.end(); + }); + }); +}); 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/utils.test.js b/test/utils.test.js index 799853b..51b3117 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -105,7 +105,90 @@ describe('Utils', function () { }); 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" + + "Network Utility Tool\n" + + "******************************************** = *********************************************\n"; + var content = Utils.generateHeaderAndFooter("Network Utility 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", + "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 () { From 3ce10157904a83d932f81a09c090d7b28d65685c Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 17:11:20 +0530 Subject: [PATCH 11/54] windows mem stats specs add --- test/stats/winStats.test.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/stats/winStats.test.js b/test/stats/winStats.test.js index b1a831f..ed2d0d8 100644 --- a/test/stats/winStats.test.js +++ b/test/stats/winStats.test.js @@ -45,7 +45,29 @@ describe('WinStats', function () { 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 + } + + 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 () { From ab17047e7847fd7b9683fa3ed5d6dc23c1f69e96 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 18:05:06 +0530 Subject: [PATCH 12/54] detecting win32 via regex for win systems --- src/stats/statsFactory.js | 3 ++- src/utils.js | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stats/statsFactory.js b/src/stats/statsFactory.js index 4821b48..15c59db 100644 --- a/src/stats/statsFactory.js +++ b/src/stats/statsFactory.js @@ -11,7 +11,8 @@ var HANDLER_MAPPING = { var StatsFactory = { getHandler: function (type) { - var handler = HANDLER_MAPPING[type] || BaseStats; + type = type.match(/linux|darwin|win/) || []; + var handler = HANDLER_MAPPING[type[0]] || BaseStats; return handler; } } diff --git a/src/utils.js b/src/utils.js index 77ace1d..30b13c7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -53,9 +53,8 @@ var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine } else if (suffix) { line = line + " " + suffix.toString().repeat(remainingCharacters); } - - return newLine ? line + os.EOL : line; } + return newLine ? line + os.EOL : line; } /** @@ -184,7 +183,7 @@ var getLongestVal = function (arr) { var safeToString = function (val) { try { - val = val.toString(); + val = val.toString() || 'empty/no data'; } catch (e) { val = JSON.stringify(val) || 'undefined'; } From 9edd094e93f3e3f4791f526ce065f087cc033ee6 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 19:48:16 +0530 Subject: [PATCH 13/54] comments across files and individual functions. CLI Command usage explanation with --help argument --- src/commandLine.js | 44 ++++++++++++++++++++++++++++- src/connectivity.js | 19 +++++++++++++ src/logger.js | 10 +++++++ src/node.js | 8 ++++++ src/server.js | 58 +++++++++++++++++++++++++++++++++++++-- src/stats/baseStats.js | 5 ++++ src/stats/linuxStats.js | 5 ++++ src/stats/macStats.js | 5 ++++ src/stats/statsFactory.js | 5 ++++ src/stats/winStats.js | 5 ++++ src/utils.js | 37 ++++++++++++++++++++++++- 11 files changed, 196 insertions(+), 5 deletions(-) diff --git a/src/commandLine.js b/src/commandLine.js index 2e20060..d71034a 100644 --- a/src/commandLine.js +++ b/src/commandLine.js @@ -1,15 +1,48 @@ +/** + * Command Line Manager to parse the command line arguments + * and set the necessary fields in NwtGlobalConfig. + */ + var constants = require('../config/constants'); var NwtGlobalConfig = constants.NwtGlobalConfig; var CommandLineManager = { + helpForArgs: function () { + var helpOutput = "\nNetwork Utility Tool - A Proxy Tool for debugging request failures leading to\n" + + "dropping of requests or not being able to reach BrowserStack.\n" + + "\n" + + "Usage: NetworkUtilityTool [ARGUMENTS]\n" + + "ARGUMENTS:\n" + + " --proxy-host : Hostname of the Proxy which is required for the client requests\n" + + " --proxy-port : Port of the Proxy which is required for the client requests\n" + + " --proxy-user : Username of the Proxy which is required for the client requests\n" + + " --proxy-pass : Password of the Proxy which is required for the client requests\n" + + " --del-logs: Deletes any existing logs from the NWT_Logs/ directory\n" + + " and initializes new files for logging. Refer 'NWT_Logs/' directory in the same directory\n" + + " where the Network Utility Tool exists\n" + + " --help: Help for Network Utility 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) { + var index = argv.indexOf('--help'); + if (index !== -1) { + CommandLineManager.helpForArgs(); + process.exit(0); + } + // Process proxy arguments - var index = argv.indexOf('--proxy-host'); + index = argv.indexOf('--proxy-host'); if (index !== -1 && CommandLineManager.validArgValue(argv[index + 1])) { NwtGlobalConfig['proxy'] = NwtGlobalConfig['proxy'] || {}; NwtGlobalConfig.proxy['host'] = argv[index + 1]; @@ -38,12 +71,21 @@ var CommandLineManager = { } } + // process arguments which decides whether existing logs should be deleted + // or appended var index = argv.indexOf('--del-logs'); if (index !== -1) { NwtGlobalConfig.deleteExistingLogs = true; + argv.splice(index, 1); } else { NwtGlobalConfig.deleteExistingLogs = false; } + + if (argv.length > 2) { + console.log("\nInvalid Arguments: ", argv.slice(2).join(', ')); + CommandLineManager.helpForArgs(); + process.exit(0); + } } } diff --git a/src/connectivity.js b/src/connectivity.js index b66836d..72135a7 100644 --- a/src/connectivity.js +++ b/src/connectivity.js @@ -1,3 +1,11 @@ +/** + * 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'); @@ -102,6 +110,11 @@ var ConnectivityChecker = { }, + /** + * 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]; @@ -137,6 +150,12 @@ var ConnectivityChecker = { } }, + /** + * 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; diff --git a/src/logger.js b/src/logger.js index 6be887d..5a258ef 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,3 +1,7 @@ +/** + * Base Logger to help initialize loggers for different purposes. + */ + var winston = require('winston'); var LogManager = { @@ -11,6 +15,12 @@ var LogManager = { }) }, + /** + * 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 () { diff --git a/src/node.js b/src/node.js index b4b1e95..bd419f1 100644 --- a/src/node.js +++ b/src/node.js @@ -1,3 +1,10 @@ +/** + * Entry point for setting up of Network Utility 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 NwtGlobalConfig = constants.NwtGlobalConfig; @@ -79,6 +86,7 @@ var NwTool = { console.log("Exiting the Tool..."); process.exit(1); } + console.log("\nRefer 'NWT_Logs' folder for CPU/Network/Memory Stats and Connectivity Checks with BrowserStack components.") }); }); } diff --git a/src/server.js b/src/server.js index b38530a..9c1e20f 100644 --- a/src/server.js +++ b/src/server.js @@ -1,3 +1,10 @@ +/** + * 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 keepAliveAgent = new http.Agent({ keepAlive: true }); var url = require('url'); @@ -9,6 +16,10 @@ var NWTHandler = { _requestCounter: 0, + /** + * Generates the request options template for firing requests based on + * whether the user had provided any proxy input or not. + */ generatorForRequestOptionsObject: function () { NWTHandler._reqObjTemplate = { method: null, @@ -52,6 +63,13 @@ var NWTHandler = { } }, + /** + * 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.REQ_FAILED_MSG; var parseSessionId = parsedRequest.path.match(/\/wd\/hub\/session\/([a-z0-9]+)\/*/); @@ -81,9 +99,12 @@ var NWTHandler = { }, /** - * - * @param {Socket} source - * @param {Socket} destination + * Reads from the source and pushes to the destination with + * backpressuring. + * Pipe can be used instead. But any sort of data access/manipulation + * will require the given format. + * @param {ReadableStream} source + * @param {WritableStream} destination * @param {Buffer} chunk */ _dataEventHandler: function (source, destination, chunk) { @@ -95,14 +116,31 @@ var NWTHandler = { } }, + /** + * Handler for Response Data + * @param {ReadableStream} source + * @param {WritableStream} destination + * @param {Buffer} chunk + */ _responseDataHandler: function (source, destination, chunk) { NWTHandler._dataEventHandler(source, destination, chunk); }, + /** + * Handler for Request Data + * @param {ReadableStream} source + * @param {WritableStream} destination + * @param {Buffer} chunk + */ _requestDataHandler: function (source, destination, chunk) { NWTHandler._dataEventHandler(source, destination, chunk); }, + /** + * Executes the HTTP request on behalf of the client request + * @param {Object} requestOptions + * @param {Function} callback + */ _executeRequest: function (requestOptions, callback) { var toolToFurtherRequest = http.request(Object.assign({}, requestOptions, { agent: keepAliveAgent }), function (response) { callback(response); @@ -111,6 +149,11 @@ var NWTHandler = { return toolToFurtherRequest; }, + /** + * Handler for incoming requests to Network Utility Tool proxy server. + * @param {} clientRequest + * @param {} clientResponse + */ requestHandler: function (clientRequest, clientResponse) { clientRequest.id = ++NWTHandler._requestCounter; @@ -199,6 +242,11 @@ var NWTHandler = { }, + /** + * Starts the proxy server on the given port + * @param {String|Number} port + * @param {Function} callback + */ startProxy: function (port, callback) { try { NWTHandler.generatorForRequestOptionsObject(); @@ -216,6 +264,10 @@ var NWTHandler = { } }, + /** + * Stops the currently running proxy server + * @param {Function} callback + */ stopProxy: function (callback) { try { if (NWTHandler.server) { diff --git a/src/stats/baseStats.js b/src/stats/baseStats.js index 9542353..3fa66d7 100644 --- a/src/stats/baseStats.js +++ b/src/stats/baseStats.js @@ -1,3 +1,8 @@ +/** + * Base stats object which is inherited by objects of other platforms for generating their + * stats object. + */ + var Utils = require('../utils'); var BaseStats = { diff --git a/src/stats/linuxStats.js b/src/stats/linuxStats.js index 4d4630f..51c1cfe 100644 --- a/src/stats/linuxStats.js +++ b/src/stats/linuxStats.js @@ -1,3 +1,8 @@ +/** + * 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'); diff --git a/src/stats/macStats.js b/src/stats/macStats.js index ec9f05b..39e248c 100644 --- a/src/stats/macStats.js +++ b/src/stats/macStats.js @@ -1,3 +1,8 @@ +/** + * 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'); diff --git a/src/stats/statsFactory.js b/src/stats/statsFactory.js index 15c59db..151586d 100644 --- a/src/stats/statsFactory.js +++ b/src/stats/statsFactory.js @@ -1,3 +1,8 @@ +/** + * Factory pattern handler generator for stats collection + * based on the platform + */ + var MacStats = require('./macStats'); var WinStats = require('./winStats'); var LinuxStats = require('./linuxStats'); diff --git a/src/stats/winStats.js b/src/stats/winStats.js index c484e67..e625f0e 100644 --- a/src/stats/winStats.js +++ b/src/stats/winStats.js @@ -1,3 +1,8 @@ +/** + * 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'); diff --git a/src/utils.js b/src/utils.js index 30b13c7..94d34f1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,14 @@ 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|Object} proxyObj + */ var proxyAuthToBase64 = function (proxyObj) { if (typeof proxyObj === 'object') { var base64Auth = Buffer.from(proxyObj.username + ":" + proxyObj.password); @@ -35,6 +43,14 @@ var fetchPropertyValue = function (content, propertyToFetch, separator) { 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 + */ var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine) { line = safeToString(line); if (line) { @@ -58,7 +74,7 @@ var formatAndBeautifyLine = function (line, prefix, suffix, idealLength, newLine } /** - * + * Generates header and footer for the given content. * @param {String} content * @param {String} title * @param {Date} generatedAt @@ -85,6 +101,13 @@ var generateHeaderAndFooter = function (content, title, generatedAt, startTime) 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"); @@ -108,6 +131,9 @@ var execMultiple = function (commands, callback) { }) } +/** + * Fetches the WMIC path in Windows + */ var getWmicPath = function () { if (os.type() === 'Windows_NT') { var wmicPath = process.env.WINDIR + '\\system32\\wbem\\wmic.exe'; @@ -129,6 +155,15 @@ var getWmicPath = function () { } } +/** + * 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 + */ var beautifyObject = function (obj, keysTitle, valuesTitle, maxKeyLength, maxValLength) { if (typeof obj !== 'object') return 'Not an Object' + os.EOL; if (Array.isArray(obj)) { From 704542e96706715e8e8b2ecfd574ba049c7e1f61 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 19:49:56 +0530 Subject: [PATCH 14/54] EOLs --- config/constants.js | 2 +- src/node.js | 2 +- src/utils.js | 2 +- test/commandLine.test.js | 2 +- test/helper.js | 2 +- test/logger.test.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/constants.js b/config/constants.js index 9f2cf47..e24a8df 100644 --- a/config/constants.js +++ b/config/constants.js @@ -99,4 +99,4 @@ module.exports.WIN = Object.freeze({ 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 -}); \ No newline at end of file +}); diff --git a/src/node.js b/src/node.js index bd419f1..3991ef6 100644 --- a/src/node.js +++ b/src/node.js @@ -92,4 +92,4 @@ var NwTool = { } } -NwTool.start(); \ No newline at end of file +NwTool.start(); diff --git a/src/utils.js b/src/utils.js index 94d34f1..2d0da5d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -239,4 +239,4 @@ module.exports = { beautifyObject, isValidCallback, safeToString -} \ No newline at end of file +} diff --git a/test/commandLine.test.js b/test/commandLine.test.js index c63bcb1..aa6c615 100644 --- a/test/commandLine.test.js +++ b/test/commandLine.test.js @@ -72,4 +72,4 @@ describe('CommandLineManager', function () { }); }); }); -}); \ No newline at end of file +}); diff --git a/test/helper.js b/test/helper.js index f4a54db..5fb0efb 100644 --- a/test/helper.js +++ b/test/helper.js @@ -48,4 +48,4 @@ module.exports = { nockGetRequest, nockProxyUrl, nockGetRequestWithError -} \ No newline at end of file +} diff --git a/test/logger.test.js b/test/logger.test.js index 3da0946..b1ab953 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -114,4 +114,4 @@ var mockLogger; LogManager.getLogger.restore(); }); }); -}); \ No newline at end of file +}); From 735e9777294eaaf8b1611e1a57f62fe8ecff3c27 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 19:56:34 +0530 Subject: [PATCH 15/54] npm start script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 399f5af..2ee2ba4 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Tool to debug failed/dropped requests at client side", "main": "src/node.js", "scripts": { + "start": "node src/node.js", "test": "nyc --reporter=html ./node_modules/mocha/bin/mocha 'test/**/*.test.js'" }, "author": "", From e69adbd624be8328a3c72db4db2ae3dd87d98b91 Mon Sep 17 00:00:00 2001 From: Rohan Chougule Date: Tue, 2 Jun 2020 20:46:55 +0530 Subject: [PATCH 16/54] pkg downgrade to support node 4. added the executables for all platforms. --- NWTool-Mac | Bin 0 -> 18675866 bytes NWTool.exe | Bin 0 -> 11737994 bytes NwTool-Linux | Bin 0 -> 19385602 bytes package.json | 5 ++++- 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100755 NWTool-Mac create mode 100644 NWTool.exe create mode 100755 NwTool-Linux diff --git a/NWTool-Mac b/NWTool-Mac new file mode 100755 index 0000000000000000000000000000000000000000..8b531610610dda5ed7dd12dd96456d25e5235d02 GIT binary patch literal 18675866 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

tDojI8fs}`fJ%VsT2$iK_xN3? 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 z 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