From 4445cfac3c94bfe7fa1ce56badcfb1ef68bcb17f Mon Sep 17 00:00:00 2001 From: "Nong (Ron) Wang" Date: Tue, 7 Apr 2020 22:32:09 -0700 Subject: [PATCH] feat: add version reminder to high-level commands if ask-cli releases (#104) --- lib/commands/abstract-command.js | 88 +++++++++++---- lib/utils/constants.js | 3 + test/functional/commands/api/index.js | 6 +- test/unit/commands/abstract-command-test.js | 113 ++++++++++++++++++++ 4 files changed, 186 insertions(+), 24 deletions(-) diff --git a/lib/commands/abstract-command.js b/lib/commands/abstract-command.js index e276fd6e..29460802 100644 --- a/lib/commands/abstract-command.js +++ b/lib/commands/abstract-command.js @@ -1,6 +1,8 @@ const os = require('os'); const path = require('path'); +const semver = require('semver'); +const httpClient = require('@src/clients/http-client'); const { validateRequiredOption, validateOptionString, @@ -11,6 +13,8 @@ const CONSTANTS = require('@src/utils/constants'); const metricClient = require('@src/utils/metrics'); const Messenger = require('@src/view/messenger'); +const packageJson = require('@root/package.json'); + /** * Base class for ASK CLI command that provides option parsing, commander configuration and option validation at runtime. */ @@ -69,31 +73,38 @@ class AbstractCommand { // set Messenger debug preferrance Messenger.getInstance().doDebug = args[0].debug; + /** + * Start metric client + */ metricClient.startAction(args[0]._name, 'command'); - // validate options - try { - this._validateOptions(args[0]); - - /** - * Since this code is ran for every command, we'll just be initiating appConfig here (no files created). - * Only `ask configure` command should have the eligibility to create the ASK config file (which is handled - * in the configure workflow). - */ - if (args[0]._name !== 'configure') { - this._initiateAppConfig(); - } - } catch (err) { - Messenger.getInstance().error(err); - resolve(); - this.exit(1); - return; - } - // execute handler logic of each command; quit execution - this.handle(...args, (error) => { - metricClient.sendData(error).then(() => { + /** + * Check if a new CLI version is released + */ + this._remindsIfNewVersion(args[0].debug, () => { + try { + this._validateOptions(args[0]); + + /** + * Since this code is ran for every command, we'll just be initiating appConfig here (no files created). + * Only `ask configure` command should have the eligibility to create the ASK config file (which is handled + * in the configure workflow). + */ + if (args[0]._name !== 'configure') { + this._initiateAppConfig(); + } + } catch (err) { + Messenger.getInstance().error(err); resolve(); - this.exit(error ? 1 : 0); + this.exit(1); + return; + } + // execute handler logic of each command; quit execution + this.handle(...args, (error) => { + metricClient.sendData(error).then(() => { + resolve(); + this.exit(error ? 1 : 0); + }); }); }); })); @@ -168,6 +179,39 @@ class AbstractCommand { } } + _remindsIfNewVersion(doDebug, callback) { + httpClient.request({ + url: `${CONSTANTS.NPM_REGISTRY_URL_BASE}/${CONSTANTS.APPLICATION_NAME}/latest`, + method: CONSTANTS.HTTP_REQUEST.VERB.GET + }, 'GET_NPM_REGISTRY', doDebug, (err, response) => { + if (err) { + Messenger.getInstance().error(`Failed to get the latest version for ${CONSTANTS.APPLICATION_NAME} from NPM registry.\n${err}\n`); + } else { + const BANNER_WITH_HASH = '##########################################################################'; + const latestVersion = JSON.parse(response.body).version; + if (packageJson.version !== latestVersion) { + if (semver.major(packageJson.version) < semver.major(latestVersion)) { + Messenger.getInstance().info(`\ +${BANNER_WITH_HASH} +[Info]: New MAJOR version (v${latestVersion}) of ${CONSTANTS.APPLICATION_NAME} is available now. Current version v${packageJson.version}. +It is recommended to use the latest version. Please update using "npm upgrade -g ${CONSTANTS.APPLICATION_NAME}". +${BANNER_WITH_HASH}\n`); + } else if ( + semver.major(packageJson.version) === semver.major(latestVersion) + && semver.minor(packageJson.version) < semver.minor(latestVersion) + ) { + Messenger.getInstance().info(`\ +${BANNER_WITH_HASH} +[Info]: New MINOR version (v${latestVersion}) of ${CONSTANTS.APPLICATION_NAME} is available now. Current version v${packageJson.version}. +It is recommended to use the latest version. Please update using "npm upgrade -g ${CONSTANTS.APPLICATION_NAME}". +${BANNER_WITH_HASH}\n`); + } + } + } + callback(); + }); + } + _initiateAppConfig() { const configFilePath = path.join(os.homedir(), CONSTANTS.FILE_PATH.ASK.HIDDEN_FOLDER, CONSTANTS.FILE_PATH.ASK.PROFILE_FILE); new AppConfig(configFilePath); diff --git a/lib/utils/constants.js b/lib/utils/constants.js index 178d59cc..11dacc27 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -1,3 +1,6 @@ +module.exports.APPLICATION_NAME = 'ask-cli-x'; +module.exports.NPM_REGISTRY_URL_BASE = 'http://registry.npmjs.org'; + module.exports.METRICS = { ENDPOINT: '', // TODO add the official endpoint when we have it NEW_USER_LENGTH_DAYS: 3 diff --git a/test/functional/commands/api/index.js b/test/functional/commands/api/index.js index afd1f073..c44d6d9a 100644 --- a/test/functional/commands/api/index.js +++ b/test/functional/commands/api/index.js @@ -4,12 +4,13 @@ const os = require('os'); const fs = require('fs-extra'); const path = require('path'); +const httpClient = require('@src/clients/http-client'); const AuthorizationController = require('@src/controllers/authorization-controller'); -const Messenger = require('@src/view/messenger'); +const { AbstractCommand } = require('@src/commands/abstract-command'); const { commander } = require('@src/commands/api/api-commander'); -const httpClient = require('@src/clients/http-client'); const CONSTANTS = require('@src/utils/constants'); const metricClient = require('@src/utils/metrics'); +const Messenger = require('@src/view/messenger'); /** * Provide static profile and token related Test data @@ -183,6 +184,7 @@ class ApiCommandBasicTest { // Mock http server // According to input httpClienConfig, // decide the httpClient behaviours + sinon.stub(AbstractCommand.prototype, '_remindsIfNewVersion').callsArgWith(1); sinon.stub(httpClient, 'request'); httpClient.request.callsArgWith(3, 'HTTP mock failed to handle the current input'); // set fallback when input misses this.httpMockConfig.forEach((config) => { diff --git a/test/unit/commands/abstract-command-test.js b/test/unit/commands/abstract-command-test.js index 2fb89253..f46ced71 100644 --- a/test/unit/commands/abstract-command-test.js +++ b/test/unit/commands/abstract-command-test.js @@ -3,12 +3,24 @@ const sinon = require('sinon'); const commander = require('commander'); const path = require('path'); +const httpClient = require('@src/clients/http-client'); const { AbstractCommand } = require('@src/commands/abstract-command'); const AppConfig = require('@src/model/app-config'); +const CONSTANTS = require('@src/utils/constants'); const metricClient = require('@src/utils/metrics'); +const Messenger = require('@src/view/messenger'); +const packageJson = require('@root/package.json'); describe('Command test - AbstractCommand class', () => { + const TEST_DO_DEBUG_FALSE = 'false'; + const TEST_HTTP_ERROR = 'http error'; + const TEST_NPM_REGISTRY_DATA = inputVersion => { + const result = { + body: JSON.stringify({ version: inputVersion }) + }; + return result; + }; const FIXTURE_PATH = path.join(process.cwd(), 'test', 'unit', 'fixture', 'model'); const APP_CONFIG_NO_PROFILES_PATH = path.join(FIXTURE_PATH, 'app-config-no-profiles.json'); @@ -48,6 +60,7 @@ describe('Command test - AbstractCommand class', () => { mockProcessExit = sinon.stub(process, 'exit'); mockConsoleError = sinon.stub(console, 'error'); sinon.stub(metricClient, 'sendData').resolves(); + sinon.stub(AbstractCommand.prototype, '_remindsIfNewVersion').callsArgWith(1); }); it('| should be able to register command', async () => { @@ -277,6 +290,106 @@ describe('Command test - AbstractCommand class', () => { }); }); + describe('# verify new version reminder method', () => { + const currentMajor = parseInt(packageJson.version.split('.')[0], 10); + const currentMinor = parseInt(packageJson.version.split('.')[1], 10); + let errorStub, warnStub, infoStub; + + beforeEach(() => { + errorStub = sinon.stub(); + warnStub = sinon.stub(); + infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub, + warn: warnStub, + error: errorStub + }); + sinon.stub(httpClient, 'request'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| http client request error, should warn it out and pass the process', (done) => { + // setup + httpClient.request.callsArgWith(3, TEST_HTTP_ERROR); + // call + AbstractCommand.prototype._remindsIfNewVersion(TEST_DO_DEBUG_FALSE, (err) => { + // verify + expect(httpClient.request.args[0][0].url).equal( + `${CONSTANTS.NPM_REGISTRY_URL_BASE}/${CONSTANTS.APPLICATION_NAME}/latest` + ); + expect(httpClient.request.args[0][0].method).equal(CONSTANTS.HTTP_REQUEST.VERB.GET); + expect(errorStub.args[0][0]).equal( + `Failed to get the latest version for ${CONSTANTS.APPLICATION_NAME} from NPM registry.\n${TEST_HTTP_ERROR}\n` + ); + expect(err).equal(undefined); + done(); + }); + }); + + it('| new major version released, should error out and pass the process', (done) => { + // setup + const latestVersion = `${currentMajor + 1}.0.0`; + httpClient.request.callsArgWith(3, null, TEST_NPM_REGISTRY_DATA(latestVersion)); + // call + AbstractCommand.prototype._remindsIfNewVersion(TEST_DO_DEBUG_FALSE, (err) => { + // verify + expect(httpClient.request.args[0][0].url).equal( + `${CONSTANTS.NPM_REGISTRY_URL_BASE}/${CONSTANTS.APPLICATION_NAME}/latest` + ); + expect(httpClient.request.args[0][0].method).equal(CONSTANTS.HTTP_REQUEST.VERB.GET); + expect(infoStub.args[0][0]).equal(`\ +########################################################################## +[Info]: New MAJOR version (v${latestVersion}) of ${CONSTANTS.APPLICATION_NAME} is available now. Current version v${packageJson.version}. +It is recommended to use the latest version. Please update using "npm upgrade -g ${CONSTANTS.APPLICATION_NAME}". +##########################################################################\n`); + expect(err).equal(undefined); + done(); + }); + }); + + it('| new minor version released, should warn out and pass the process', (done) => { + // setup + const latestVersion = `${currentMajor}.${currentMinor + 1}.0`; + httpClient.request.callsArgWith(3, null, TEST_NPM_REGISTRY_DATA(latestVersion)); + // call + AbstractCommand.prototype._remindsIfNewVersion(TEST_DO_DEBUG_FALSE, (err) => { + // verify + expect(httpClient.request.args[0][0].url).equal( + `${CONSTANTS.NPM_REGISTRY_URL_BASE}/${CONSTANTS.APPLICATION_NAME}/latest` + ); + expect(httpClient.request.args[0][0].method).equal(CONSTANTS.HTTP_REQUEST.VERB.GET); + expect(infoStub.args[0][0]).equal(`\ +########################################################################## +[Info]: New MINOR version (v${latestVersion}) of ${CONSTANTS.APPLICATION_NAME} is available now. Current version v${packageJson.version}. +It is recommended to use the latest version. Please update using "npm upgrade -g ${CONSTANTS.APPLICATION_NAME}". +##########################################################################\n`); + expect(err).equal(undefined); + done(); + }); + }); + + it('| version is latest, should do nothing and pass the process', (done) => { + // setup + httpClient.request.callsArgWith(3, null, TEST_NPM_REGISTRY_DATA(`${currentMajor}.${currentMinor}.0`)); + // call + AbstractCommand.prototype._remindsIfNewVersion(TEST_DO_DEBUG_FALSE, (err) => { + // verify + expect(httpClient.request.args[0][0].url).equal( + `${CONSTANTS.NPM_REGISTRY_URL_BASE}/${CONSTANTS.APPLICATION_NAME}/latest` + ); + expect(httpClient.request.args[0][0].method).equal(CONSTANTS.HTTP_REQUEST.VERB.GET); + expect(infoStub.callCount).equal(0); + expect(warnStub.callCount).equal(0); + expect(errorStub.callCount).equal(0); + expect(err).equal(undefined); + done(); + }); + }); + }); + describe('# check AppConfig object ', () => { const mockOptionModel = { 'foo-option': {