From ae6f1111e2292bee4a6bebd72214bd3103d8f8a0 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 23 Sep 2023 20:30:59 +0200 Subject: [PATCH] :sparkles: Command Line Interface Usage example: ``` npm run cli -- --help npm run cli -- --version npm run cli -- --login npm run cli -- --balance ``` --- README.md | 10 +- package-lock.json | 255 ++++++++++++++++++++++++++++- package.json | 11 +- src/cli.test.ts | 400 ++++++++++++++++++++++++++++++++++++++++++++++ src/cli.ts | 183 +++++++++++++++++++++ src/constants.ts | 4 +- src/library.ts | 2 +- 7 files changed, 860 insertions(+), 5 deletions(-) create mode 100644 src/cli.test.ts create mode 100755 src/cli.ts diff --git a/README.md b/README.md index 1f76a03..d0be348 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Unofficial Laboral Kutxa JS library npm install laboral-kutxa ``` -## Usage +## Library Usage Reading through the `misProductos` list: @@ -55,3 +55,11 @@ Output: financing: { cantidad: 123456.78, moneda: 'EUR' } } ``` + +## CLI Usage + +It's also possible to consume the CLI directly to access the account. + +```sh +npx laboral-kutxa --balance +``` diff --git a/package-lock.json b/package-lock.json index 423cd35..b81c5d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.5", "license": "MIT", "dependencies": { - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "read": "^1.0.7" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -17,6 +18,8 @@ "@types/mocha": "^10.0.1", "@types/nock": "^11.1.0", "@types/node-fetch": "^2.6.4", + "@types/proxyquire": "^1.3.29", + "@types/read": "^0.0.29", "@types/sinon": "^10.0.16", "@types/sinon-chai": "^3.2.9", "canvas": "^2.11.2", @@ -28,6 +31,7 @@ "nock": "^13.3.3", "nyc": "^15.1.0", "prettier": "^3.0.3", + "proxyquire": "^2.1.3", "sinon": "^15.2.0", "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", @@ -834,6 +838,18 @@ "form-data": "^3.0.0" } }, + "node_modules/@types/proxyquire": { + "version": "1.3.29", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.29.tgz", + "integrity": "sha512-8/JYXN9NmE4tEGUU/JI7FcvloTu7CxYkb01h4kI+HRvABxwpleLXsvVmOF85LlgEb/xwe+H8MwM4s3ushNuffg==", + "dev": true + }, + "node_modules/@types/read": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/read/-/read-0.0.29.tgz", + "integrity": "sha512-TisW3O3OhpP8/ZwaiqV7kewh9gnoH7PfqHd4hkCM9ogiqWEagu43WXpHWqgPbltXhembYJDpYB3cVwUIOweHXg==", + "dev": true + }, "node_modules/@types/sinon": { "version": "10.0.16", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz", @@ -1803,6 +1819,19 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dev": true, + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1963,6 +1992,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, "node_modules/gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -2120,6 +2155,18 @@ "node": ">=6" } }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2281,6 +2328,18 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2320,6 +2379,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -2868,6 +2936,12 @@ "node": ">= 12" } }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3023,12 +3097,23 @@ "node": ">=0.3.1" } }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", + "dev": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, "node_modules/nan": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", @@ -3474,6 +3559,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -3616,6 +3707,17 @@ "node": ">= 8" } }, + "node_modules/proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -3656,6 +3758,17 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3785,6 +3898,23 @@ "dev": true, "peer": true }, + "node_modules/resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -4126,6 +4256,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -5368,6 +5510,18 @@ "form-data": "^3.0.0" } }, + "@types/proxyquire": { + "version": "1.3.29", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.29.tgz", + "integrity": "sha512-8/JYXN9NmE4tEGUU/JI7FcvloTu7CxYkb01h4kI+HRvABxwpleLXsvVmOF85LlgEb/xwe+H8MwM4s3ushNuffg==", + "dev": true + }, + "@types/read": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/read/-/read-0.0.29.tgz", + "integrity": "sha512-TisW3O3OhpP8/ZwaiqV7kewh9gnoH7PfqHd4hkCM9ogiqWEagu43WXpHWqgPbltXhembYJDpYB3cVwUIOweHXg==", + "dev": true + }, "@types/sinon": { "version": "10.0.16", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz", @@ -6105,6 +6259,16 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6213,6 +6377,12 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, "gauge": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", @@ -6335,6 +6505,15 @@ "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", @@ -6459,6 +6638,15 @@ "binary-extensions": "^2.0.0" } }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6486,6 +6674,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -6908,6 +7102,12 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -7020,12 +7220,23 @@ } } }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", + "dev": true + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, "nan": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", @@ -7368,6 +7579,12 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -7470,6 +7687,17 @@ "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7504,6 +7732,14 @@ "safe-buffer": "^5.1.0" } }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "requires": { + "mute-stream": "~0.0.4" + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7609,6 +7845,17 @@ "dev": true, "peer": true }, + "resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -7855,6 +8102,12 @@ "has-flag": "^4.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index b4f1c91..552a1ef 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,16 @@ "description": "Unofficial Laboral Kutxa JS library", "main": "dist/index.js", "types": "dist/index.d.ts", + "bin": { + "laboral-kutxa": "dist/cli.js" + }, "scripts": { "test": "mocha -r ts-node/register src/*.test.ts", "test:debug": "node --inspect-brk ./node_modules/.bin/mocha -r ts-node/register src/*.test.ts", "test:coverage": "nyc mocha -r ts-node/register -r jsdom-global/register src/*.test.ts", "test:coveralls": "npm run test:coverage && coveralls < coverage/lcov.info", + "cli": "ts-node src/cli.ts", + "cli:inspect": "node --inspect --require ts-node/register src/cli.ts", "build": "tsc", "lint": "prettier --check '{src,.github}/**/*.{ts,yml}' *.md", "format": "prettier --write '{src,.github}/**/*.{ts,yml}' *.md", @@ -49,6 +54,8 @@ "@types/mocha": "^10.0.1", "@types/nock": "^11.1.0", "@types/node-fetch": "^2.6.4", + "@types/proxyquire": "^1.3.29", + "@types/read": "^0.0.29", "@types/sinon": "^10.0.16", "@types/sinon-chai": "^3.2.9", "canvas": "^2.11.2", @@ -60,6 +67,7 @@ "nock": "^13.3.3", "nyc": "^15.1.0", "prettier": "^3.0.3", + "proxyquire": "^2.1.3", "sinon": "^15.2.0", "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", @@ -67,6 +75,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "read": "^1.0.7" } } diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..c7d9d60 --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,400 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as pq from "proxyquire"; +import * as sinon from "sinon"; +import * as api from "./library"; +import * as cli from "./cli"; +import { APPLICATION_NAME } from "./constants"; + +const proxyquire = pq.noCallThru(); + +describe("asyncRead", () => { + let readStub: sinon.SinonStub; + let cliProxy: any; + + beforeEach(() => { + readStub = sinon.stub(); + cliProxy = proxyquire("./cli", { read: readStub }); + }); + + it("should resolve with result on successful read", async () => { + const expected = "successful read"; + readStub.callsFake((_, callback) => callback(null, expected)); + const result = await cliProxy.asyncRead({ prompt: "Enter something: " }); + expect(result).to.equal(expected); + }); + + it("should reject with error on failed read", async () => { + const expectedError = new Error("failed read"); + readStub.callsFake((_, callback) => callback(expectedError)); + return expect( + cliProxy.asyncRead({ prompt: "Enter something: " }), + ).to.eventually.be.rejectedWith(expectedError); + }); +}); + +describe("promptLogin", () => { + let readStub: sinon.SinonStub; + let cliProxy: any; + + beforeEach(() => { + readStub = sinon.stub(); + cliProxy = proxyquire("./cli", { read: readStub }); + }); + + it("should prompt for username and password", async () => { + const expectedUsername = "username"; + const expectedPassword = "password"; + readStub + .onCall(0) + .callsFake((_, callback: (err: any, result: string) => void) => + callback(null, expectedUsername), + ); + readStub + .onCall(1) + .callsFake((_, callback: (err: any, result: string) => void) => + callback(null, expectedPassword), + ); + const { username, password } = await cliProxy.promptLogin(); + expect(username).to.equal(expectedUsername); + expect(password).to.equal(expectedPassword); + expect(readStub).to.have.been.calledTwice; + }); +}); + +describe("userCacheDir", () => { + const originalProcess = process; + const testCases: { platform: NodeJS.Platform; expected: string }[] = [ + { platform: "linux", expected: "/.cache/someapp" }, + { platform: "darwin", expected: "/Library/Caches/someapp" }, + { platform: "win32", expected: "/AppData/Local/someapp/Cache" }, + { platform: "unknown" as any, expected: "/.cache/someapp" }, + ]; + + testCases.forEach(({ platform, expected }) => { + it(`should return correct path for ${platform}`, () => { + sinon.stub(process, "platform").value(platform); + const result = cli.userCacheDir("someapp"); + expect(result) + .to.be.a("string") + .and.satisfy((res: string) => res.endsWith(expected)); + }); + }); +}); + +describe("getSessionCachePath", () => { + it("base", () => { + const expected = "/laboral-kutxa/session.cache"; + const result = cli.getSessionCachePath(); + expect(result) + .to.be.a("string") + .and.satisfy((res: string) => res.endsWith(expected)); + }); +}); + +describe("getCachedSessionInfo", () => { + let readFileSyncStub: sinon.SinonStub; + let cliProxy: any; + + beforeEach(() => { + readFileSyncStub = sinon.stub(); + cliProxy = proxyquire("./cli", { fs: { readFileSync: readFileSyncStub } }); + }); + + it("base", () => { + const expected = { foo: "bar" }; + readFileSyncStub.returns(Buffer.from(JSON.stringify(expected))); + const result = cliProxy.getCachedSessionInfo(); + expect(result).to.deep.equal(expected); + expect(readFileSyncStub).to.have.been.calledOnce; + }); +}); + +describe("cacheSessionInfo", () => { + let mkdirSyncStub: sinon.SinonStub; + let writeFileSyncStub: sinon.SinonStub; + let cliProxy: any; + + beforeEach(() => { + mkdirSyncStub = sinon.stub(); + writeFileSyncStub = sinon.stub(); + cliProxy = proxyquire("./cli", { + fs: { mkdirSync: mkdirSyncStub, writeFileSync: writeFileSyncStub }, + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should cache session info correctly", () => { + const token = "token"; + const expectedPath = "path/to/session"; + const expectedSessionInfo = { token }; + sinon.stub(cliProxy, "getSessionCachePath").returns(expectedPath); + cliProxy.cacheSessionInfo(token); + expect(mkdirSyncStub).to.have.been.calledOnceWith( + path.dirname(expectedPath), + { recursive: true }, + ); + expect(writeFileSyncStub).to.have.been.calledOnceWith( + expectedPath, + JSON.stringify(expectedSessionInfo), + ); + }); +}); + +describe("login", () => { + let promptLoginStub: sinon.SinonStub; + let loginStub: sinon.SinonStub; + + beforeEach(() => { + promptLoginStub = sinon.stub(cli, "promptLogin"); + loginStub = sinon.stub(api, "login"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should prompt for login and return a token", async () => { + const expectedToken = "token"; + const username = "username"; + const password = "password"; + promptLoginStub.resolves({ username, password }); + loginStub.resolves(expectedToken); + const token = await cli.login(); + expect(token).to.equal(expectedToken); + expect(promptLoginStub).to.have.been.calledOnce; + expect(loginStub).to.have.been.calledOnceWith(username, password); + }); +}); + +describe("processLogin", () => { + let loginStub: sinon.SinonStub; + let cacheSessionInfoStub: sinon.SinonStub; + + beforeEach(() => { + loginStub = sinon.stub(cli, "login"); + cacheSessionInfoStub = sinon.stub(cli, "cacheSessionInfo"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call login and cacheSessionInfo with correct arguments and return token", async () => { + const expectedToken = "token"; + loginStub.resolves({ token: expectedToken }); + const actualToken = await cli.processLogin(); + expect(actualToken).to.equal(expectedToken); + expect(loginStub).to.have.been.calledOnce; + expect(cacheSessionInfoStub).to.have.been.calledOnceWith(expectedToken); + }); +}); + +describe("getSessionOrLogin", () => { + let getCachedSessionInfoStub: sinon.SinonStub; + let processLoginStub: sinon.SinonStub; + + beforeEach(() => { + getCachedSessionInfoStub = sinon.stub(cli, "getCachedSessionInfo"); + processLoginStub = sinon.stub(cli, "processLogin"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return token from cache if session is cached", async () => { + const expectedToken = "token"; + getCachedSessionInfoStub.resolves({ token: expectedToken }); + const actualToken = await cli.getSessionOrLogin(); + expect(actualToken).to.equal(expectedToken); + expect(getCachedSessionInfoStub).to.have.been.calledOnce; + expect(processLoginStub).to.not.have.been.called; + }); + + it("should call processLogin if no session is cached", async () => { + const expectedToken = "token"; + getCachedSessionInfoStub.rejects(); + processLoginStub.resolves(expectedToken); + const actualToken = await cli.getSessionOrLogin(); + expect(actualToken).to.equal(expectedToken); + expect(getCachedSessionInfoStub).to.have.been.calledOnce; + expect(processLoginStub).to.have.been.calledOnce; + }); +}); + +describe("generateBalanceStrings", () => { + it("should correctly generate balance strings and filter out zero balances", () => { + const importes = { + account1: { cantidad: 100, moneda: "USD" }, + account2: { cantidad: 0, moneda: "USD" }, + account3: { cantidad: 200, moneda: "EUR" }, + } as any; + const expected = ["account1: 100 USD", "account3: 200 EUR"]; + const actual = cli.generateBalanceStrings(importes); + expect(actual).to.eql(expected); + }); + + it("should return an empty array when all balances are zero", () => { + const importes = { + account1: { cantidad: 0, moneda: "USD" }, + account2: { cantidad: 0, moneda: "USD" }, + } as any; + const expected: string[] = []; + const actual = cli.generateBalanceStrings(importes); + expect(actual).to.eql(expected); + }); + + it("should return an empty array when there is no account", () => { + const importes = {} as any; + const expected: string[] = []; + const actual = cli.generateBalanceStrings(importes); + expect(actual).to.eql(expected); + }); +}); + +describe("printBalance", () => { + let logSpy: sinon.SinonSpy; + + beforeEach(() => { + logSpy = sinon.spy(console, "log"); + }); + + afterEach(() => { + logSpy.restore(); + }); + + it("should correctly print non-zero balances", () => { + const importes = { + account1: { cantidad: 100, moneda: "USD" }, + account2: { cantidad: 0, moneda: "USD" }, + account3: { cantidad: 200, moneda: "EUR" }, + } as any; + cli.printBalance(importes); + expect(logSpy.calledTwice).to.be.true; + expect(logSpy.calledWith("account1: 100 USD")).to.be.true; + expect(logSpy.calledWith("account3: 200 EUR")).to.be.true; + }); +}); + +describe("processBalance", () => { + let getSessionOrLoginStub: sinon.SinonStub; + let getMyProductsStub: sinon.SinonStub; + let logSpy: sinon.SinonSpy; + let printBalanceStub: sinon.SinonStub; + + beforeEach(() => { + getSessionOrLoginStub = sinon.stub(cli, "getSessionOrLogin"); + getMyProductsStub = sinon.stub(api, "getMyProducts"); + logSpy = sinon.spy(console, "log"); + printBalanceStub = sinon.stub(cli, "printBalance"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call the dependent methods with correct arguments", async () => { + const fakeToken = "token"; + getSessionOrLoginStub.resolves(fakeToken); + const fakeProducts = { + _Importes: { + account1: { cantidad: 100, moneda: "USD" }, + }, + }; + getMyProductsStub.resolves(fakeProducts); + await cli.processBalance(); + expect(getSessionOrLoginStub.calledOnce).to.be.true; + expect(getMyProductsStub.calledOnceWith(fakeToken)).to.be.true; + expect(printBalanceStub.calledOnceWith(fakeProducts._Importes)).to.be.true; + }); +}); + +describe("version", () => { + let logSpy: sinon.SinonSpy; + let originalVersion: string | undefined; + + beforeEach(() => { + originalVersion = process.env.npm_package_version; + process.env.npm_package_version = "1.0.0-test"; + logSpy = sinon.spy(console, "log"); + }); + + afterEach(() => { + process.env.npm_package_version = originalVersion; + logSpy.restore(); + }); + + it("should log the correct version", () => { + cli.version(); + expect(logSpy.calledOnceWith("1.0.0-test")).to.be.true; + }); +}); + +describe("help", () => { + let logStub: sinon.SinonStub; + + beforeEach(() => { + logStub = sinon.stub(console, "log"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should print the help message", () => { + cli.help(); + expect(logStub.calledOnce).to.be.true; + }); +}); + +describe("main", () => { + let processLoginStub: sinon.SinonStub; + let processBalanceStub: sinon.SinonStub; + let helpStub: sinon.SinonStub; + let versionStub: sinon.SinonStub; + + beforeEach(() => { + processLoginStub = sinon.stub(cli, "processLogin"); + processBalanceStub = sinon.stub(cli, "processBalance"); + helpStub = sinon.stub(cli, "help"); + versionStub = sinon.stub(cli, "version"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call processLogin when argv includes --login", () => { + cli.main(["node", "script.js", "--login"]); + expect(processLoginStub.calledOnce).to.be.true; + }); + + it("should call processBalance when argv includes --balance", () => { + cli.main(["node", "script.js", "--balance"]); + expect(processBalanceStub.calledOnce).to.be.true; + }); + + it("should call help when argv includes --help or is invalid", () => { + cli.main(["node", "script.js", "--help"]); + expect(helpStub.calledOnce).to.be.true; + helpStub.resetHistory(); + cli.main(["node", "script.js", "--invalidArg"]); + expect(helpStub.calledOnce).to.be.true; + }); + + it("should call version when argv includes --version", () => { + cli.main(["node", "script.js", "--version"]); + expect(versionStub.calledOnce).to.be.true; + }); + + it("should call help by default when no argument is provided", () => { + cli.main(["node", "script.js"]); + expect(helpStub.calledOnce).to.be.true; + }); +}); diff --git a/src/cli.ts b/src/cli.ts new file mode 100755 index 0000000..8943d7c --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,183 @@ +#!/usr/bin/env node +import * as os from "os"; +import * as path from "path"; +import * as fs from "fs"; +import read from "read"; +import * as api from "./index"; +import { Amount, Amounts } from "./types"; +import { APPLICATION_NAME, SESSION_CACHE_FILENAME } from "./constants"; + +const asyncRead = (options: any): Promise => + new Promise((resolve, reject) => + read( + options, + (error: any, result: string) => + (error && reject(error)) || resolve(result), + ), + ); + +/* + * Prompt user for credentials and the return them. + */ +const promptLogin = async () => { + const username = await exports.asyncRead({ prompt: "username: " }); + const password = await exports.asyncRead({ + prompt: "password: ", + silent: true, + }); + return { username, password }; +}; + +const getCacheDirWin = (appname: string): string => + path.join(os.homedir(), "AppData", "Local", appname, "Cache"); + +const getCacheDirMac = (appname: string): string => + path.join(os.homedir(), "Library", "Caches", appname); + +const getCacheDirUnix = (appname: string): string => + path.join(os.homedir(), ".cache", appname); + +const platformCacheDirMap: Record string> = { + win32: getCacheDirWin, + darwin: getCacheDirMac, + default: getCacheDirUnix, +}; + +const getUserCacheDirFn: () => (appname: string) => string = () => + platformCacheDirMap[os.platform()] || platformCacheDirMap.default; + +/* + * Return OS dependent base data directory (Python user_cache_dir() port). + */ +const userCacheDir = (appname: string): string => getUserCacheDirFn()(appname); + +/* + * Return file path used to store the session cookie. + */ +const getSessionCachePath = () => + path.join(exports.userCacheDir(APPLICATION_NAME), SESSION_CACHE_FILENAME); + +/* + * Return session token from cache. + */ +const getCachedSessionInfo = () => + JSON.parse(fs.readFileSync(exports.getSessionCachePath()).toString()); + +/* + * Cache session token to disk. + */ +const cacheSessionInfo = (token: string) => { + const sessionCachePath = exports.getSessionCachePath(); + const cachedSessionInfo = { + token, + }; + fs.mkdirSync(path.dirname(sessionCachePath), { recursive: true }); + fs.writeFileSync(sessionCachePath, JSON.stringify(cachedSessionInfo)); +}; + +/* + * Login and return session token. + */ +const login = async () => { + const { username, password } = await exports.promptLogin(); + const token = await api.login(username, password); + return token; +}; + +/* + * Login and cache the token. + */ +const processLogin = async () => { + const { token } = await exports.login(); + exports.cacheSessionInfo(token); + return token; +}; + +/* + * Retrieve session from cache or prompt login then store token. + */ +const getSessionOrLogin = async () => { + try { + const { token } = await exports.getCachedSessionInfo(); + return token; + } catch (error: unknown) { + return exports.processLogin(); + } +}; + +const generateBalanceStrings = (importes: Amounts): string[] => + Object.entries(importes) + .filter(([, { cantidad }]) => cantidad !== 0) + .map( + ([accountType, { cantidad, moneda }]) => + `${accountType}: ${cantidad} ${moneda}`, + ); + +/* + * Print per "importe" balance if not zero. + */ +const printBalance = (importes: Amounts): void => + exports + .generateBalanceStrings(importes) + .forEach((balance: Amount) => console.log(balance)); + +/* + * Retrieve and print balance per card. + */ +const processBalance = async () => { + const token = await exports.getSessionOrLogin(); + const products = await api.getMyProducts(token); + exports.printBalance(products._Importes); +}; + +const version = () => console.log(process.env.npm_package_version); + +const help = () => { + console.log( + // eslint-disable-line no-console + "Usage:\n" + + "laboralkutxa --help\tthis message\n" + + "laboralkutxa --login\tlogins and caches the session\n" + + "laboralkutxa --balance\tprints non zero account balances\n" + + "laboralkutxa --version\tprints the version", + ); +}; + +const main = (argv: string[]) => { + const args = argv.slice(2); + const arg2FunctionMap: { [key: string]: any } = { + login: exports.processLogin, + balance: exports.processBalance, + help: exports.help, + version: exports.version, + }; + // defaults to help if unknown + const arg2Function = (arg: string) => arg2FunctionMap[arg] || exports.help; + // defaults to help if no args provided + const arg = args[0] ? args[0].replace(/^--/, "") : "help"; + const fun = arg2Function(arg); + fun(); +}; + +const mainIsModule = (module: any, main: NodeModule) => main === module; + +export { + asyncRead, + promptLogin, + userCacheDir, + getSessionCachePath, + getCachedSessionInfo, + cacheSessionInfo, + login, + processLogin, + getSessionOrLogin, + generateBalanceStrings, + printBalance, + processBalance, + help, + version, + main, +}; + +/* istanbul ignore next */ +mainIsModule(require.main, module) && main(process.argv); diff --git a/src/constants.ts b/src/constants.ts index 85cee88..986187e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ const API_URL = "https://lkweb.laboralkutxa.com"; +const APPLICATION_NAME = "laboral-kutxa"; +const SESSION_CACHE_FILENAME = "session.cache"; -export { API_URL }; +export { API_URL, APPLICATION_NAME, SESSION_CACHE_FILENAME }; diff --git a/src/library.ts b/src/library.ts index 716f68d..8bae60e 100644 --- a/src/library.ts +++ b/src/library.ts @@ -1,6 +1,6 @@ import assert from "assert"; import fetch, { Response } from "node-fetch"; -import { API_URL } from "./constants"; +import { API_URL } from "./index"; import { LoginResponse, MyProductsResponse, Product } from "./types"; const handleStatusCode = async (response: Response) => {