diff --git a/package.json b/package.json index 2706e84c..80f20973 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "got": "^11.8.2", "marked-terminal": "^4.2.0", "semver": "^7.3.5", + "sinon-chai": "^3.7.0", "tslib": "^2" }, "devDependencies": { diff --git a/src/commands/info/releasenotes/display.ts b/src/commands/info/releasenotes/display.ts index 9ce5ebc0..1d15d115 100644 --- a/src/commands/info/releasenotes/display.ts +++ b/src/commands/info/releasenotes/display.ts @@ -8,7 +8,7 @@ // Needed this to ensure the "helpers" were decalred before read in examples /* eslint-disable @typescript-eslint/member-ordering */ -import axios from 'axios'; +import got from 'got'; import { Env } from '@salesforce/kit'; import { flags, SfdxCommand } from '@salesforce/command'; import { getString } from '@salesforce/ts-types'; @@ -17,6 +17,8 @@ import { Messages } from '@salesforce/core'; import { getInfoConfig, InfoConfig } from '../../../shared/get-info-config'; import { getReleaseNotes } from '../../../shared/get-release-notes'; +import { PLUGIN_INFO_GET_TIMEOUT } from '../../../constants'; + // Initialize Messages with the current plugin directory Messages.importMessagesDirectory(__dirname); @@ -63,8 +65,6 @@ export default class Display extends SfdxCommand { let infoConfig: InfoConfig; try { - // this.config.root should be cross platform, it is set here: - // https://github.com/salesforcecli/sfvm/blob/2211d7b7b34cb21f6b738dc31ca27ef2e46de1cb/src/api/installation.ts#L111 infoConfig = await getInfoConfig(this.config.root); } catch (err) { const msg = getString(err, 'message'); @@ -80,9 +80,16 @@ export default class Display extends SfdxCommand { if (Display.helpers.includes(version)) { try { - const { data } = await axios.get(distTagUrl); + const options = { timeout: PLUGIN_INFO_GET_TIMEOUT }; + + type DistTagJson = { + latest: string; + 'latest-rc': string; + }; + + const body = await got(distTagUrl, options).json(); - version = version.includes('rc') ? data['latest-rc'] : data['latest']; + version = version.includes('rc') ? body['latest-rc'] : body['latest']; } catch (err) { // TODO: Could fallback up using npm here? That way private cli repos could auth with .npmrc // -- could use this: https://github.com/salesforcecli/plugin-trust/blob/0393b906a30e8858816625517eda5db69377c178/src/lib/npmCommand.ts @@ -99,11 +106,12 @@ export default class Display extends SfdxCommand { } catch (err) { const msg = getString(err, 'message'); - this.ux.warn(`Release notes GET request failed with message:\n${msg}`); + this.ux.warn(`getReleaseNotes() request failed with message:\n${msg}`); return; } + // temp until markdown parser is added this.ux.log(releaseNotes); } } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..dbfb8862 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +export const PLUGIN_INFO_GET_TIMEOUT = (process.env.PLUGIN_INFO_GET_TIMEOUT || 3000) as number; diff --git a/src/shared/get-info-config.ts b/src/shared/get-info-config.ts index e44d40bd..dfa5ebeb 100644 --- a/src/shared/get-info-config.ts +++ b/src/shared/get-info-config.ts @@ -10,7 +10,7 @@ import { readJson } from 'fs-extra'; import { PJSON } from '@oclif/config'; import { get } from '@salesforce/ts-types'; -interface PjsonWithInfo extends PJSON { +export interface PjsonWithInfo extends PJSON { oclif: PJSON['oclif'] & { info: InfoConfig; }; @@ -39,8 +39,8 @@ Add to oclif object } */ -export async function getInfoConfig(root: string): Promise { - const fullPath = join(root, 'package.json'); +export async function getInfoConfig(path: string): Promise { + const fullPath = join(path, 'package.json'); const json = (await readJson(fullPath)) as PjsonWithInfo; diff --git a/src/shared/get-release-notes.ts b/src/shared/get-release-notes.ts index a29ac242..75ba1e4e 100644 --- a/src/shared/get-release-notes.ts +++ b/src/shared/get-release-notes.ts @@ -5,27 +5,26 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +import got from 'got'; import { major } from 'semver'; +import { PLUGIN_INFO_GET_TIMEOUT } from '../constants'; -export async function getReleaseNotes(base: string, filename: string, version: string): Promise { +export async function getReleaseNotes(base: string, filename: string, version: string): Promise { const majorVersion = major(version); - const options: AxiosRequestConfig = { - timeout: 5000, - validateStatus: () => true, + const options = { + timeout: PLUGIN_INFO_GET_TIMEOUT, + throwHttpErrors: false, }; const getPromises = [ - axios.get(`${base}/v${majorVersion}.md`, options), - axios.get(`${base}/${filename}`, options), + got(`${base}/v${majorVersion}.md`, options), + got(`${base}/${filename}`, { ...options, throwHttpErrors: true }), ]; const [versioned, readme] = await Promise.all(getPromises); - const { data } = versioned.status === 200 ? versioned : readme; + const { body } = versioned.statusCode === 200 ? versioned : readme; - // check readme status too - - return data; + return body; } diff --git a/test/commands/hello/org.test.ts b/test/commands/hello/org.test.ts deleted file mode 100644 index 7e3ad774..00000000 --- a/test/commands/hello/org.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect, test } from '@salesforce/command/lib/test'; -import { ensureJsonMap, ensureString } from '@salesforce/ts-types'; - -describe('hello:org', () => { - test - .withOrg({ username: 'test@org.com' }, true) - .withConnectionRequest((request) => { - const requestMap = ensureJsonMap(request); - if (ensureString(requestMap.url).includes('Organization')) { - return Promise.resolve({ - records: [ - { - Name: 'Super Awesome Org', - TrialExpirationDate: '2018-03-20T23:24:11.000+0000', - }, - ], - }); - } - return Promise.resolve({ records: [] }); - }) - .stdout() - .command(['hello:org', '--targetusername', 'test@org.com']) - .it('runs hello:org --targetusername test@org.com', (ctx) => { - expect(ctx.stdout).to.contain( - 'Hello world! This is org: Super Awesome Org and I will be around until Tue Mar 20 2018!' - ); - }); -}); diff --git a/test/shared/get-info-config.test.ts b/test/shared/get-info-config.test.ts new file mode 100644 index 00000000..4f1acb53 --- /dev/null +++ b/test/shared/get-info-config.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import * as pathPkg from 'path'; +import { expect, use as chaiUse } from 'chai'; +import * as Sinon from 'sinon'; +import * as SinonChai from 'sinon-chai'; +import { stubMethod, spyMethod } from '@salesforce/ts-sinon'; +import * as fsExtra from 'fs-extra'; +import { getString } from '@salesforce/ts-types'; +import { getInfoConfig, PjsonWithInfo } from '../../src/shared/get-info-config'; + +chaiUse(SinonChai); + +describe('getInfoConfig tests', () => { + let sandbox: sinon.SinonSandbox; + let readJsonStub: Sinon.SinonStub; + let joinSpy: Sinon.SinonSpy; + + let pjsonMock: PjsonWithInfo; + + const path = 'path/to'; + + beforeEach(() => { + pjsonMock = { + name: 'testing', + version: '1.2.3', + oclif: { + info: { + releasenotes: { + distTagUrl: 'https://registry.npmjs.org/-/package/sfdx-cli/dist-tags', + releaseNotesPath: 'https://raw.githubusercontent.com/forcedotcom/cli/main/releasenotes/sfdx', + releaseNotesFilename: 'README.md', + }, + }, + }, + }; + + sandbox = Sinon.createSandbox(); + readJsonStub = stubMethod(sandbox, fsExtra, 'readJson').returns(pjsonMock); + joinSpy = spyMethod(sandbox, pathPkg, 'join'); + }); + + afterEach(() => { + joinSpy.restore(); + readJsonStub.restore(); + sandbox.restore(); + }); + + it('join is called with path arg and package.json', async () => { + await getInfoConfig(path); + + expect(joinSpy.args[0]).to.deep.equal([path, 'package.json']); + expect(joinSpy.returnValues[0]).to.equal(`${path}/package.json`); + }); + + it('calls readJson with pjson path', async () => { + await getInfoConfig(path); + + expect(readJsonStub.args[0][0]).to.deep.equal(`${path}/package.json`); + }); + + it('info config is extracted from package.json', async () => { + const info = await getInfoConfig(path); + + expect(info).to.deep.equal(pjsonMock.oclif.info); + }); + + it('throws an error if info config does not exist', async () => { + readJsonStub.returns({ oclif: {} }); + + try { + await getInfoConfig(path); + } catch (err) { + const msg = getString(err, 'message'); + + expect(msg).to.equal('getInfoConfig() failed to find pjson.oclif.info config'); + } + }); +}); diff --git a/test/shared/get-release-notes.test.ts b/test/shared/get-release-notes.test.ts new file mode 100644 index 00000000..2f0fd3ba --- /dev/null +++ b/test/shared/get-release-notes.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2018, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import got from 'got'; +import { expect, use as chaiUse } from 'chai'; +import * as Sinon from 'sinon'; +import * as SinonChai from 'sinon-chai'; +import * as semver from 'semver'; +import { stubMethod, spyMethod } from '@salesforce/ts-sinon'; +import { getReleaseNotes } from '../../src/shared/get-release-notes'; +import { PLUGIN_INFO_GET_TIMEOUT } from '../../src/constants'; + +chaiUse(SinonChai); + +type gotResponse = { + statusCode: number; + body: string; +}; + +describe('getReleaseNotes tests', () => { + let sandbox: sinon.SinonSandbox; + let gotStub: sinon.SinonStub; + let semverSpy: Sinon.SinonSpy; + + let path: string; + let version: string; + let filename: string; + let options; + let versionedResponse: gotResponse; + let readmeResponse: gotResponse; + + beforeEach(() => { + path = 'https://example.com'; + version = '1.2.3'; + filename = 'readme.md'; + options = { + timeout: PLUGIN_INFO_GET_TIMEOUT, + throwHttpErrors: false, + }; + versionedResponse = { + statusCode: 200, + body: 'versioned response body', + }; + readmeResponse = { + statusCode: 200, + body: 'readme response body', + }; + + sandbox = Sinon.createSandbox(); + gotStub = stubMethod(sandbox, got, 'default'); + semverSpy = spyMethod(sandbox, semver, 'major'); + + gotStub.onCall(0).returns(versionedResponse); + gotStub.onCall(1).returns(readmeResponse); + }); + + afterEach(() => { + semverSpy.restore(); + gotStub.restore(); + sandbox.restore(); + }); + + it('semver.major is called passed version', async () => { + await getReleaseNotes(path, filename, version); + + expect(semverSpy.args[0][0]).to.equal(version); + expect(semverSpy.returnValues[0]).to.equal(1); + }); + + it('makes versioned GET request with correct args', async () => { + await getReleaseNotes(path, filename, version); + + const expected = [`${path}/v1.md`, options]; + + expect(gotStub.args[0]).to.deep.equal(expected); + }); + + it('makes readme GET request with correct args', async () => { + await getReleaseNotes(path, filename, version); + + const expected = [`${path}/${filename}`, { ...options, throwHttpErrors: true }]; + + expect(gotStub.args[1]).to.deep.equal(expected); + }); + + it('returns versioned markdown if found', async () => { + const body = await getReleaseNotes(path, filename, version); + + expect(body).to.equal('versioned response body'); + }); + + it('returns readme markdown if versioned markdown is not found', async () => { + versionedResponse.statusCode = 404; + + const body = await getReleaseNotes(path, filename, version); + + expect(body).to.equal('readme response body'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 4b0c49e4..45fe288d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6139,6 +6139,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +sinon-chai@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" + integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== + sinon@10.0.0, sinon@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-10.0.0.tgz#52279f97e35646ff73d23207d0307977c9b81430"