diff --git a/.circleci/config.yml b/.circleci/config.yml index 8729ec07..72318c1a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,6 +60,17 @@ workflows: node_version: lts - os: windows node_version: maintenance + - release-management/test-nut: + name: nuts-on-linux + sfdx_version: latest + requires: + - release-management/test-package + # - release-management/test-nut: + # name: nuts-on-windows + # sfdx_version: latest + # os: windows + # requires: + # - release-management/test-package - release-management/release-package: sign: true github-release: true diff --git a/package.json b/package.json index 0e9aae4e..41913bc7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@salesforce/prettier-config": "^0.0.2", "@salesforce/ts-sinon": "1.3.21", "@types/shelljs": "^0.8.9", + "@types/sinon-chai": "^3.2.5", "@typescript-eslint/eslint-plugin": "^4.2.0", "@typescript-eslint/parser": "^4.2.0", "chai": "^4.2.0", @@ -47,6 +48,7 @@ "pretty-quick": "^3.1.0", "shx": "0.3.3", "sinon": "10.0.0", + "sinon-chai": "^3.7.0", "ts-node": "^10.0.0", "typescript": "^4.1.3" }, @@ -114,7 +116,7 @@ "test": "sf-test", "test:command-reference": "./bin/run commandreference:generate --erroronwarnings", "test:deprecation-policy": "./bin/run snapshot:compare", - "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel", + "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000", "version": "oclif-dev readme" }, "husky": { diff --git a/src/commands/plugins/trust/verify.ts b/src/commands/plugins/trust/verify.ts index 59b1d18c..0aa51c26 100644 --- a/src/commands/plugins/trust/verify.ts +++ b/src/commands/plugins/trust/verify.ts @@ -47,9 +47,10 @@ export class Verify extends SfdxCommand { const vConfig = new VerificationConfig(); const configContext: ConfigContext = { - cacheDir: get(this.config, 'configDir') as string, - configDir: get(this.config, 'cacheDir') as string, + cacheDir: get(this.config, 'cacheDir') as string, + configDir: get(this.config, 'configDir') as string, dataDir: get(this.config, 'dataDir') as string, + cliRoot: get(this.config, 'root') as string, }; this.logger.debug(`cacheDir: ${configContext.cacheDir}`); diff --git a/src/hooks/verifyInstallSignature.ts b/src/hooks/verifyInstallSignature.ts index 4faede6d..08ccf64e 100644 --- a/src/hooks/verifyInstallSignature.ts +++ b/src/hooks/verifyInstallSignature.ts @@ -58,6 +58,7 @@ export const hook: Hook.PluginsPreinstall = async function (options) { cacheDir: options.config.cacheDir, configDir: options.config.configDir, dataDir: options.config.dataDir, + cliRoot: options.config.root, }; const vConfig = VerificationConfigBuilder.build(npmName, configContext); diff --git a/src/lib/installationVerification.ts b/src/lib/installationVerification.ts index 06aabb73..8b8d3ad3 100644 --- a/src/lib/installationVerification.ts +++ b/src/lib/installationVerification.ts @@ -29,6 +29,7 @@ export interface ConfigContext { configDir?: string; cacheDir?: string; dataDir?: string; + cliRoot?: string; } export interface Verifier { verify(): Promise; @@ -306,7 +307,9 @@ export class InstallationVerification implements Verifier { // Make sure the cache path exists. try { await fs.mkdirp(this.getCachePath()); - new NpmModule(npmMeta.moduleName, npmMeta.version).pack(getNpmRegistry().href, { cwd: this.getCachePath() }); + new NpmModule(npmMeta.moduleName, npmMeta.version, this.config.cliRoot).pack(getNpmRegistry().href, { + cwd: this.getCachePath(), + }); const tarBallFile = fs .readdirSync(this.getCachePath(), { withFileTypes: true }) .find((entry) => entry.isFile() && entry.name.includes(npmMeta.version)); @@ -345,7 +348,7 @@ export class InstallationVerification implements Verifier { ? `@${this.pluginNpmName.scope}/${this.pluginNpmName.name}` : this.pluginNpmName.name; - const npmModule = new NpmModule(npmShowModule, this.pluginNpmName.tag); + const npmModule = new NpmModule(npmShowModule, this.pluginNpmName.tag, this.config.cliRoot); const npmMetadata = npmModule.show(npmRegistry.href); logger.debug('retrieveNpmMeta | Found npm meta information.'); if (!npmMetadata.versions) { diff --git a/src/lib/npmCommand.ts b/src/lib/npmCommand.ts index 33bf0389..aa9890f0 100644 --- a/src/lib/npmCommand.ts +++ b/src/lib/npmCommand.ts @@ -6,9 +6,12 @@ */ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { type as osType } from 'os'; import * as path from 'path'; -import * as shelljs from 'shelljs'; + import npmRunPath from 'npm-run-path'; +import * as shelljs from 'shelljs'; + import { SfdxError, fs } from '@salesforce/core'; export type NpmMeta = { @@ -39,6 +42,7 @@ export type NpmShowResults = { type NpmCommandOptions = shelljs.ExecOptions & { json?: boolean; registry?: string; + cliRoot?: string; }; type NpmCommandResult = NpmShowResults & { @@ -55,9 +59,10 @@ export class NpmCommand { private static npmPkgPath = require.resolve('npm/package.json'); public static runNpmCmd(cmd: string, options = {} as NpmCommandOptions): NpmCommandResult { + const nodeExecutable = NpmCommand.findNode(options.cliRoot); const npmCli = NpmCommand.npmCli(); - const exec = `${npmCli} ${cmd} --registry=${options.registry} --json`; - const npmShowResult = shelljs.exec(exec, { + const command = `"${nodeExecutable}" "${npmCli}" ${cmd} --registry=${options.registry} --json`; + const npmShowResult = shelljs.exec(command, { ...options, silent: true, fatal: true, @@ -78,28 +83,88 @@ export class NpmCommand { return this.npmPkgPath; } + /** + * Returns the path to the npm-cli.js file in this package's node_modules + * + * @private + */ private static npmCli(): string { const pkgPath = NpmCommand.npmPackagePath(); const pkgJson = fs.readJsonSync(pkgPath) as NpmPackage; const prjPath = pkgPath.substring(0, pkgPath.lastIndexOf(path.sep)); return path.join(prjPath, pkgJson.bin['npm']); } + + /** + * Locate node executable and return its absolute path + * First it tries to locate the node executable on the root path passed in + * If not found then tries to use whatver 'node' resolves to on the user's PATH + * If found return absolute path to the executable + * If the node executable cannot be found, an error is thrown + * + * @private + */ + private static findNode(root: string = undefined): string { + const isExecutable = (filepath: string): boolean => { + if (osType() === 'Windows_NT') return filepath.endsWith('node.exe'); + + try { + if (filepath.endsWith('node')) { + // This checks if the filepath is executable on Mac or Linux, if it is not it errors. + fs.accessSync(filepath, fs.constants.X_OK); + return true; + } + } catch { + return false; + } + return false; + }; + + if (root) { + const sfdxBinDirs = NpmCommand.findSfdxBinDirs(root); + if (sfdxBinDirs.length > 0) { + // Find the node executable + const node = shelljs.find(sfdxBinDirs).filter((file) => isExecutable(file))[0]; + if (node) { + return fs.realpathSync(node); + } + } + } + + // Check to see if node is installed + const nodeShellString: shelljs.ShellString = shelljs.which('node'); + if (nodeShellString?.code === 0 && nodeShellString?.stdout) return nodeShellString.stdout; + + throw new SfdxError('Cannot locate node executable.', 'CannotFindNodeExecutable'); + } + + /** + * Finds the bin directory in the sfdx installation root path + * + * @param sfdxPath + * @private + */ + private static findSfdxBinDirs(sfdxPath: string): string[] { + return sfdxPath + ? [path.join(sfdxPath, 'bin'), path.join(sfdxPath, 'client', 'bin')].filter((p) => fs.existsSync(p)) + : []; + } } export class NpmModule { public npmMeta: NpmMeta; - public constructor(private module: string, private version: string = 'latest') { + public constructor(private module: string, private version: string = 'latest', private cliRoot: string = undefined) { this.npmMeta = { moduleName: module, }; } public show(registry: string): NpmShowResults { - return NpmCommand.runNpmCmd(`show ${this.module}@${this.version}`, { registry }); + return NpmCommand.runNpmCmd(`show ${this.module}@${this.version}`, { registry, cliRoot: this.cliRoot }); } public pack(registry: string, options?: shelljs.ExecOptions): void { - NpmCommand.runNpmCmd(`pack ${this.module}@${this.version}`, { ...options, registry }); + NpmCommand.runNpmCmd(`pack ${this.module}@${this.version}`, { ...options, registry, cliRoot: this.cliRoot }); return; } } diff --git a/test/lib/installationVerification.test.ts b/test/lib/installationVerification.test.ts index 7a020826..39c12443 100644 --- a/test/lib/installationVerification.test.ts +++ b/test/lib/installationVerification.test.ts @@ -68,6 +68,18 @@ const getShelljsExecStub = ( stderr, stdout: JSON.stringify(PACK_RESULT), }; + } else if (cmd.includes('node')) { + return { + code: 0, + stderr, + stdout: 'node', + }; + } else if (cmd.includes('sfdx')) { + return { + code: 0, + stderr, + stdout: 'sfdx', + }; } else { throw new Error(`Unexpected test cmd - ${cmd}`); } @@ -105,12 +117,17 @@ describe('InstallationVerification Tests', () => { get configDir() { return 'configDir'; }, + get cliRoot() { + return __dirname; + }, }; const currentRegistry = process.env.SFDX_NPM_REGISTRY; + let fsReaddirSyncStub: Sinon.SinonStub; let plugin: NpmName; + let realpathSyncStub: Sinon.SinonStub; let sandbox: sinon.SinonSandbox; let shelljsExecStub: Sinon.SinonStub; - let fsReaddirSyncStub: Sinon.SinonStub; + let shelljsFindStub: Sinon.SinonStub; beforeEach(() => { sandbox = Sinon.createSandbox(); @@ -122,11 +139,15 @@ describe('InstallationVerification Tests', () => { }, }, ]); + realpathSyncStub = stubMethod(sandbox, fs, 'realpathSync').returns('node.exe'); + shelljsFindStub = stubMethod(sandbox, shelljs, 'find').returns(['node.exe']); plugin = NpmName.parse('foo'); }); afterEach(() => { fsReaddirSyncStub.restore(); + realpathSyncStub.restore(); + shelljsFindStub.restore(); if (shelljsExecStub) { shelljsExecStub.restore(); } diff --git a/test/lib/npmCommand.test.ts b/test/lib/npmCommand.test.ts index 4ec7df56..b55d314d 100644 --- a/test/lib/npmCommand.test.ts +++ b/test/lib/npmCommand.test.ts @@ -6,12 +6,17 @@ */ import { fail } from 'assert'; -import { expect } from 'chai'; -import Sinon = require('sinon'); +import * as os from 'os'; +import { expect, use as chaiUse } from 'chai'; +import * as Sinon from 'sinon'; +import * as SinonChai from 'sinon-chai'; import * as shelljs from 'shelljs'; import { stubMethod } from '@salesforce/ts-sinon'; +import { fs } from '@salesforce/core'; import { NpmModule } from '../../src/lib/npmCommand'; +chaiUse(SinonChai); + const DEFAULT_REGISTRY = 'https://registry.npmjs.org/'; const MODULE_NAME = '@salesforce/plugin-source'; const MODULE_VERSION = '1.0.0'; @@ -48,13 +53,19 @@ const PACK_RESULT = [ ], }, ]; +const NODE_NAME = 'node'; +const NODE_PATH = `/usr/local/sfdx/bin/${NODE_NAME}`; describe('should run npm commands', () => { let sandbox: sinon.SinonSandbox; + let realpathSyncStub: Sinon.SinonStub; let shelljsExecStub: Sinon.SinonStub; + let shelljsFindStub: Sinon.SinonStub; beforeEach(() => { sandbox = Sinon.createSandbox(); + realpathSyncStub = stubMethod(sandbox, fs, 'realpathSync').returns('node.exe'); + shelljsFindStub = stubMethod(sandbox, shelljs, 'find').returns(['node.exe']); shelljsExecStub = stubMethod(sandbox, shelljs, 'exec').callsFake((cmd: string) => { expect(cmd).to.be.a('string').and.not.to.be.empty; if (cmd.includes('show')) { @@ -67,6 +78,16 @@ describe('should run npm commands', () => { code: 0, stdout: JSON.stringify(PACK_RESULT), }; + } else if (cmd.includes('node')) { + return { + code: 0, + stdout: 'node', + }; + } else if (cmd.includes('sfdx')) { + return { + code: 0, + stdout: 'sfdx', + }; } else { throw new Error(`Unexpected test cmd - ${cmd}`); } @@ -74,33 +95,174 @@ describe('should run npm commands', () => { }); afterEach(() => { + realpathSyncStub.restore(); + shelljsFindStub.restore(); + shelljsExecStub.restore(); sandbox.restore(); }); it('Runs the show command', () => { - const npmMetadata = new NpmModule(MODULE_NAME).show(DEFAULT_REGISTRY); - expect(shelljsExecStub.callCount).to.equal(1); + const npmMetadata = new NpmModule(MODULE_NAME, undefined, __dirname).show(DEFAULT_REGISTRY); + expect(shelljsExecStub).to.have.been.calledOnce; expect(shelljsExecStub.firstCall.args[0]).to.include(`show ${MODULE_NAME}@latest`); expect(shelljsExecStub.firstCall.args[0]).to.include(`--registry=${DEFAULT_REGISTRY}`); expect(npmMetadata).to.deep.equal(SHOW_RESULT); }); it('Runs the show command with specified version', () => { - const npmMetadata = new NpmModule(MODULE_NAME, MODULE_VERSION).show(DEFAULT_REGISTRY); - expect(shelljsExecStub.callCount).to.equal(1); + const npmMetadata = new NpmModule(MODULE_NAME, MODULE_VERSION, __dirname).show(DEFAULT_REGISTRY); + expect(shelljsExecStub).to.have.been.calledOnce; expect(shelljsExecStub.firstCall.args[0]).to.include(`show ${MODULE_NAME}@${MODULE_VERSION}`); expect(shelljsExecStub.firstCall.args[0]).to.include(`--registry=${DEFAULT_REGISTRY}`); expect(npmMetadata).to.deep.equal(SHOW_RESULT); }); it('Runs the pack command', () => { - new NpmModule(MODULE_NAME, MODULE_VERSION).pack(DEFAULT_REGISTRY, { cwd: CACHE_PATH }); - expect(shelljsExecStub.callCount).to.equal(1); + new NpmModule(MODULE_NAME, MODULE_VERSION, __dirname).pack(DEFAULT_REGISTRY, { cwd: CACHE_PATH }); + expect(shelljsExecStub).to.have.been.calledOnce; expect(shelljsExecStub.firstCall.args[0]).to.include(`pack ${MODULE_NAME}@${MODULE_VERSION}`); expect(shelljsExecStub.firstCall.args[0]).to.include(`--registry=${DEFAULT_REGISTRY}`); }); }); +describe('should find the node executable', () => { + let sandbox: sinon.SinonSandbox; + let shelljsExecStub: Sinon.SinonStub; + let shelljsFindStub: Sinon.SinonStub; + let shelljsWhichStub: Sinon.SinonStub; + let existsSyncStub: Sinon.SinonStub; + let realpathSyncStub: Sinon.SinonStub; + let osTypeStub: Sinon.SinonStub; + let accessSyncStub: Sinon.SinonStub; + + beforeEach(() => { + sandbox = Sinon.createSandbox(); + shelljsExecStub = stubMethod(sandbox, shelljs, 'exec').callsFake((cmd: string) => { + expect(cmd).to.be.a('string').and.not.to.be.empty; + if (cmd.includes('show')) { + return { + code: 0, + stdout: JSON.stringify(SHOW_RESULT), + }; + } else if (cmd.includes('pack')) { + return { + code: 0, + stdout: JSON.stringify(PACK_RESULT), + }; + } else if (cmd.includes('node')) { + return { + code: 0, + stdout: 'node', + }; + } else if (cmd.includes('sfdx')) { + return { + code: 0, + stdout: 'sfdx', + }; + } else { + throw new Error(`Unexpected test cmd - ${cmd}`); + } + }); + shelljsFindStub = stubMethod(sandbox, shelljs, 'find').callsFake((filePaths: string[]) => { + expect(filePaths).to.be.a('array').and.to.have.length.greaterThan(0); + return [NODE_PATH]; + }); + realpathSyncStub = stubMethod(sandbox, fs, 'realpathSync').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0); + return NODE_PATH; + }); + existsSyncStub = stubMethod(sandbox, fs, 'existsSync').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0); + return true; + }); + accessSyncStub = stubMethod(sandbox, fs, 'accessSync').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0); + return undefined; + }); + osTypeStub = stubMethod(sandbox, os, 'type').callsFake(() => 'Linux'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('finds node binary inside sfdx bin folder and runs npm show command', () => { + const npmMetadata = new NpmModule(MODULE_NAME, undefined, __dirname).show(DEFAULT_REGISTRY); + expect(accessSyncStub).to.have.been.calledOnce; + expect(existsSyncStub).to.have.been.calledTwice; + expect(osTypeStub).to.have.been.calledOnce; + expect(realpathSyncStub).to.have.been.calledOnce; + expect(shelljsExecStub).to.have.been.calledOnce; + expect(shelljsFindStub).to.have.been.calledOnce; + expect(shelljsExecStub.firstCall.args[0]).to.include(NODE_PATH); + expect(shelljsExecStub.firstCall.args[0]).to.include(`show ${MODULE_NAME}@latest`); + expect(shelljsExecStub.firstCall.args[0]).to.include(`--registry=${DEFAULT_REGISTRY}`); + expect(npmMetadata).to.deep.equal(SHOW_RESULT); + }); + + it('finds node binary inside sfdx bin folder on windows and runs npm show command', () => { + shelljsFindStub.returns(['C:\\Program Files\\sfdx\\client\\bin\\node.exe']); + osTypeStub.returns('Windows_NT'); + + const npmMetadata = new NpmModule(MODULE_NAME, undefined, __dirname).show(DEFAULT_REGISTRY); + expect(accessSyncStub).to.not.have.been.called; + expect(existsSyncStub).to.have.been.calledTwice; + expect(osTypeStub).to.have.been.calledOnce; + expect(realpathSyncStub).to.have.been.calledOnce; + expect(shelljsExecStub).to.have.been.calledOnce; + expect(shelljsFindStub).to.have.been.calledOnce; + expect(shelljsExecStub.firstCall.args[0]).to.include(NODE_PATH); + expect(shelljsExecStub.firstCall.args[0]).to.include(`show ${MODULE_NAME}@latest`); + expect(shelljsExecStub.firstCall.args[0]).to.include(`--registry=${DEFAULT_REGISTRY}`); + expect(npmMetadata).to.deep.equal(SHOW_RESULT); + }); + + it('fails to find node binary inside sfdx bin folder and falls back to global node and runs npm show command', () => { + existsSyncStub.restore(); + existsSyncStub = stubMethod(sandbox, fs, 'existsSync').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0); + return false; + }); + shelljsWhichStub = stubMethod(sandbox, shelljs, 'which').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0).and.to.be.equal('node'); + return { + stdout: NODE_PATH, + code: 0, + } as shelljs.ShellString; + }); + const npmMetadata = new NpmModule(MODULE_NAME, undefined, __dirname).show(DEFAULT_REGISTRY); + expect(existsSyncStub).to.have.been.calledTwice; + expect(shelljsFindStub).to.not.have.been.called; + expect(realpathSyncStub).to.not.have.been.called; + expect(shelljsWhichStub).to.have.been.calledOnce; + expect(shelljsExecStub).to.have.been.calledOnce; + expect(shelljsExecStub.firstCall.args[0]).to.include(NODE_NAME); + expect(shelljsExecStub.firstCall.args[0]).to.include(`show ${MODULE_NAME}@latest`); + expect(shelljsExecStub.firstCall.args[0]).to.include(`--registry=${DEFAULT_REGISTRY}`); + expect(npmMetadata).to.deep.equal(SHOW_RESULT); + }); + + it('fails to find node binary and throws', () => { + existsSyncStub.restore(); + existsSyncStub = stubMethod(sandbox, fs, 'existsSync').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0); + return false; + }); + shelljsWhichStub.restore(); + shelljsWhichStub = stubMethod(sandbox, shelljs, 'which').callsFake((filePath: string) => { + expect(filePath).to.be.a('string').and.to.have.length.greaterThan(0).and.to.be.equal('node'); + return null; + }); + try { + const npmMetadata = new NpmModule(MODULE_NAME, undefined, __dirname).show(DEFAULT_REGISTRY); + expect(npmMetadata).to.be.undefined; + fail('Error'); + } catch (error) { + expect(error.code).to.equal('CannotFindNodeExecutable'); + } + }); +}); + describe('should run npm commands with execution errors', () => { let sandbox: sinon.SinonSandbox; @@ -120,6 +282,16 @@ describe('should run npm commands with execution errors', () => { stderr: 'command execution error', stdout: '', }; + } else if (cmd.includes('node')) { + return { + code: 0, + stdout: 'node', + }; + } else if (cmd.includes('sfdx')) { + return { + code: 0, + stdout: 'sfdx', + }; } else { throw new Error(`Unexpected test cmd - ${cmd}`); } @@ -132,7 +304,7 @@ describe('should run npm commands with execution errors', () => { it('show command throws error', () => { try { - const npmMetadata = new NpmModule(MODULE_NAME).show(DEFAULT_REGISTRY); + const npmMetadata = new NpmModule(MODULE_NAME, undefined, __dirname).show(DEFAULT_REGISTRY); expect(npmMetadata).to.be.undefined; fail('Error'); } catch (error) { @@ -142,7 +314,7 @@ describe('should run npm commands with execution errors', () => { it('Runs the pack command', () => { try { - new NpmModule(MODULE_NAME, MODULE_VERSION).pack(DEFAULT_REGISTRY, { cwd: CACHE_PATH }); + new NpmModule(MODULE_NAME, MODULE_VERSION, __dirname).pack(DEFAULT_REGISTRY, { cwd: CACHE_PATH }); fail('Error'); } catch (error) { expect(error.code).to.equal('ShellExecError'); @@ -167,6 +339,16 @@ describe('should run npm commands with parse errors', () => { code: 0, stdout: 'not a json string', }; + } else if (cmd.includes('node')) { + return { + code: 0, + stdout: 'node', + }; + } else if (cmd.includes('sfdx')) { + return { + code: 0, + stdout: 'sfdx', + }; } else { throw new Error(`Unexpected test cmd - ${cmd}`); } @@ -179,7 +361,7 @@ describe('should run npm commands with parse errors', () => { it('show command throws error', () => { try { - const npmMetadata = new NpmModule(MODULE_NAME).show(DEFAULT_REGISTRY); + const npmMetadata = new NpmModule(MODULE_NAME, MODULE_VERSION, __dirname).show(DEFAULT_REGISTRY); expect(npmMetadata).to.be.undefined; fail('Error'); } catch (error) { @@ -189,7 +371,7 @@ describe('should run npm commands with parse errors', () => { it('Runs the pack command', () => { try { - new NpmModule(MODULE_NAME, MODULE_VERSION).pack(DEFAULT_REGISTRY, { cwd: CACHE_PATH }); + new NpmModule(MODULE_NAME, MODULE_VERSION, __dirname).pack(DEFAULT_REGISTRY, { cwd: CACHE_PATH }); fail('Error'); } catch (error) { expect(error.code).to.equal('ShellParseError'); diff --git a/test/nuts/plugin-install.nut.ts b/test/nuts/plugin-install.nut.ts index b08f72c9..f42a7cf1 100644 --- a/test/nuts/plugin-install.nut.ts +++ b/test/nuts/plugin-install.nut.ts @@ -6,22 +6,31 @@ */ import * as path from 'path'; +import * as os from 'os'; import { expect } from 'chai'; import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; import { fs } from '@salesforce/core'; const SIGNED_MODULE_NAME = '@salesforce/plugin-user'; -const UNSIGNED_MODULE_NAME = 'sfdx-jayree'; +const UNSIGNED_MODULE_NAME = '@mshanemc/plugin-streaming'; let session: TestSession; describe('plugins:install commands', () => { before(async () => { session = await TestSession.create(); + await fs.mkdirp(path.join(session.homeDir, '.sfdx')); + await fs.writeJson(path.join(session.homeDir, '.sfdx', 'acknowledgedUsageCollection.json'), { + acknowledged: true, + }); }); after(async () => { await session?.zip(undefined, 'artifacts'); - await session?.clean(); + try { + await session?.clean(); + } catch (error) { + // ignore + } }); it('plugins:install signed plugin', () => { @@ -33,33 +42,43 @@ describe('plugins:install commands', () => { }); it('plugins:install prompts on unsigned plugin (denies)', () => { - process.env.TESTKIT_EXECUTABLE_PATH = 'sfdx'; - const result = execCmd(`plugins:install ${UNSIGNED_MODULE_NAME}`, { - ensureExitCode: 2, // code 2 is the output code for the NO answer - answers: ['N'], - }); - expect(result.shellOutput.stderr).to.contain( - 'This plugin is not digitally signed and its authenticity cannot be verified. Continue installation y/n?:' - ); - expect(result.shellOutput.stderr).to.contain('The user canceled the plugin installation'); + // windows does not support answering the prompt + if (os.type() !== 'Windows_NT') { + process.env.TESTKIT_EXECUTABLE_PATH = 'sfdx'; + const result = execCmd(`plugins:install ${UNSIGNED_MODULE_NAME}`, { + ensureExitCode: 2, // code 2 is the output code for the NO answer + answers: ['N'], + }); + expect(result.shellOutput.stderr).to.contain( + 'This plugin is not digitally signed and its authenticity cannot be verified. Continue installation y/n?:' + ); + expect(result.shellOutput.stderr).to.contain('The user canceled the plugin installation'); + } }); it('plugins:install prompts on unsigned plugin (accepts)', () => { - process.env.TESTKIT_EXECUTABLE_PATH = 'sfdx'; - const result = execCmd(`plugins:install ${UNSIGNED_MODULE_NAME}`, { - ensureExitCode: 0, - answers: ['Y'], - }); - expect(result.shellOutput.stderr).to.contain( - 'This plugin is not digitally signed and its authenticity cannot be verified. Continue installation y/n?:' - ); - expect(result.shellOutput.stdout).to.contain('Finished digital signature check'); + // windows does not support answering the prompt + if (os.type() !== 'Windows_NT') { + process.env.TESTKIT_EXECUTABLE_PATH = 'sfdx'; + const result = execCmd(`plugins:install ${UNSIGNED_MODULE_NAME}`, { + ensureExitCode: 0, + answers: ['Y'], + }); + expect(result.shellOutput.stderr).to.contain( + 'This plugin is not digitally signed and its authenticity cannot be verified. Continue installation y/n?:' + ); + expect(result.shellOutput.stdout).to.contain('Finished digital signature check'); + } }); }); describe('plugins:install commands', () => { before(async () => { session = await TestSession.create(); + await fs.mkdirp(path.join(session.homeDir, '.sfdx')); + await fs.writeJson(path.join(session.homeDir, '.sfdx', 'acknowledgedUsageCollection.json'), { + acknowledged: true, + }); const configDir = path.join(session.homeDir, '.config', 'sfdx'); fs.mkdirSync(configDir, { recursive: true }); fs.writeJsonSync(path.join(configDir, 'unsignedPluginAllowList.json'), [UNSIGNED_MODULE_NAME]); @@ -72,7 +91,11 @@ describe('plugins:install commands', () => { after(async () => { await session?.zip(undefined, 'artifacts'); - await session?.clean(); + try { + await session?.clean(); + } catch (error) { + // ignore + } }); it('plugins:install unsigned plugin in the allow list', () => { diff --git a/yarn.lock b/yarn.lock index 1e844854..413ea59d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1050,6 +1050,14 @@ "@types/glob" "*" "@types/node" "*" +"@types/sinon-chai@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.5.tgz#df21ae57b10757da0b26f512145c065f2ad45c48" + integrity sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + "@types/sinon@*", "@types/sinon@10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.0.tgz#eecc3847af03d45ffe53d55aaaaf6ecb28b5e584" @@ -6913,6 +6921,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: version "10.0.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-10.0.0.tgz#52279f97e35646ff73d23207d0307977c9b81430"