Skip to content

Commit

Permalink
feat: add version reminder to high-level commands if ask-cli releases (
Browse files Browse the repository at this point in the history
  • Loading branch information
RonWang authored Apr 8, 2020
1 parent 0c7750c commit 4445cfa
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 24 deletions.
88 changes: 66 additions & 22 deletions lib/commands/abstract-command.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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);
});
});
});
}));
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions lib/utils/constants.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 4 additions & 2 deletions test/functional/commands/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
113 changes: 113 additions & 0 deletions test/unit/commands/abstract-command-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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': {
Expand Down

0 comments on commit 4445cfa

Please sign in to comment.