From 4e9f345ce13617a1055cd67c7b8251b77230f862 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele <willieruemmele@gmail.com> Date: Thu, 23 May 2024 12:29:00 -0600 Subject: [PATCH] feat: add network check and UT (#746) * feat: add network check and UT * chore: remove registry checks, moved to plugin-trust * docs: update ping check message --- package.json | 1 + src/commands/doctor.ts | 1 - src/diagnostics.ts | 51 ++++++++++++++++++++++++++++------ src/doctor.ts | 6 ++-- test/diagnostics.test.ts | 59 +++++++++++++++++++++++++++++++++++++++- yarn.lock | 2 +- 6 files changed, 105 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index c143bd09..a20dc1ca 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { "@inquirer/input": "^2.1.6", + "@jsforce/jsforce-node": "^3.2.0", "@oclif/core": "^3.26.5", "@salesforce/core": "^7.3.5", "@salesforce/kit": "^3.1.0", diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 4f5a45ab..7ec5217d 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -58,7 +58,6 @@ export default class Doctor extends SfCommand<SfDoctorDiagnosis> { public async run(): Promise<SfDoctorDiagnosis> { const { flags } = await this.parse(Doctor); - // this.doctor = SFDoctor.getInstance(); this.doctor = SFDoctor.init(this.config); const lifecycle = Lifecycle.getInstance(); diff --git a/src/diagnostics.ts b/src/diagnostics.ts index cf1a295b..68f5807f 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -6,22 +6,16 @@ */ import childProcess from 'node:child_process'; - +import { got } from 'got'; import { Interfaces } from '@oclif/core'; import { Lifecycle, Messages } from '@salesforce/core'; +import { Connection } from '@jsforce/jsforce-node'; import { SfDoctor, SfDoctorDiagnosis } from './doctor.js'; -// const SUPPORTED_SHELLS = [ -// 'bash', -// 'zsh', -// 'powershell' -// 'cmd.exe' -// ]; - export type DiagnosticStatus = { testName: string; status: 'pass' | 'fail' | 'warn' | 'unknown'; -} +}; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-info', 'diagnostics'); @@ -128,6 +122,45 @@ export class Diagnostics { await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName, status }); } + public async networkCheck(): Promise<void> { + await Promise.all( + [ + // salesforce endpoints + 'https://test.salesforce.com', + 'https://appexchange.salesforce.com/services/data', + ].map(async (url) => { + try { + const conn = new Connection(); + await conn.request(url); + await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: `can access: ${url}`, status: 'pass' }); + } catch (e) { + await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: `can't access: ${url}`, status: 'fail' }); + this.doctor.addSuggestion( + `Cannot reach ${url} - potential network configuration error, check proxies, firewalls, environment variables` + ); + } + }) + ); + // our S3 bucket, use the buildmanifest to avoid downloading the entire CLI + const manifestUrl = + 'https://developer.salesforce.com/media/salesforce-cli/sf/channels/stable/sf-win32-x64-buildmanifest'; + try { + await got.get(manifestUrl); + await Lifecycle.getInstance().emit('Doctor:diagnostic', { + testName: `can access: ${manifestUrl}`, + status: 'pass', + }); + } catch (e) { + await Lifecycle.getInstance().emit('Doctor:diagnostic', { + testName: `can't access: ${manifestUrl}`, + status: 'fail', + }); + this.doctor.addSuggestion( + `Cannot reach ${manifestUrl} - potential network configuration error, check proxies, firewalls, environment variables` + ); + } + } + /** * Checks and warns if any plugins are linked. */ diff --git a/src/doctor.ts b/src/doctor.ts index 6d895944..522074db 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -31,7 +31,7 @@ export type SfDoctor = { writeFileSync(filePath: string, contents: string): string; writeStderr(contents: string): Promise<boolean>; writeStdout(contents: string): Promise<boolean>; -} +}; type CliConfig = Partial<Interfaces.Config> & { nodeEngine: string }; @@ -46,7 +46,7 @@ export type SfDoctorDiagnosis = { commandName?: string; commandExitCode?: string | number; logFilePaths: string[]; -} +}; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-info', 'doctor'); @@ -67,7 +67,7 @@ export class Doctor implements SfDoctor { public readonly id: number; // Contains all gathered data and results of diagnostics. - private diagnosis: SfDoctorDiagnosis; + private readonly diagnosis: SfDoctorDiagnosis; private stdoutWriteStream: fs.WriteStream | undefined; private stderrWriteStream: fs.WriteStream | undefined; diff --git a/test/diagnostics.test.ts b/test/diagnostics.test.ts index 9e9a5024..57560209 100644 --- a/test/diagnostics.test.ts +++ b/test/diagnostics.test.ts @@ -12,6 +12,7 @@ import { fromStub, spyMethod, stubInterface, stubMethod } from '@salesforce/ts-s import { Config, Interfaces } from '@oclif/core'; import { Lifecycle } from '@salesforce/core'; import { ux } from '@oclif/core'; +import { Connection } from '@jsforce/jsforce-node'; import { Doctor } from '../src/doctor.js'; import { Diagnostics } from '../src/diagnostics.js'; @@ -87,7 +88,7 @@ describe('Diagnostics', () => { const results = diagnostics.run(); // This will have to be updated with each new test - expect(results.length).to.equal(4); + expect(results.length).to.equal(5); expect(childProcessExecStub.called).to.be.true; expect(lifecycleEmitSpy.called).to.be.true; expect(lifecycleEmitSpy.args[0][0]).to.equal('Doctor:diagnostic'); @@ -95,6 +96,62 @@ describe('Diagnostics', () => { expect(lifecycleEmitSpy.args[0][1]).to.have.property('status'); }); + describe('networkCheck', () => { + it('passes when all URLs can be reached', async () => { + stubMethod(sandbox, Connection.prototype, 'request').resolves('{}'); + + const dr = Doctor.init(oclifConfig); + const diagnostics = new Diagnostics(dr, oclifConfig); + await diagnostics.networkCheck(); + + expect(drAddSuggestionSpy.called).to.be.false; + expect(lifecycleEmitSpy.called).to.be.true; + expect(lifecycleEmitSpy.args[0][1]).to.deep.equal({ + status: 'pass', + testName: 'can access: https://test.salesforce.com', + }); + }); + + it('fails when one URL cannot be reached', async () => { + stubMethod(sandbox, Connection.prototype, 'request').rejects( + '{"error":"unsupported_grant_type","error_description":"grant type not supported"}' + ); + + const dr = Doctor.init(oclifConfig); + const diagnostics = new Diagnostics(dr, oclifConfig); + await diagnostics.networkCheck(); + + expect(drAddSuggestionSpy.called).to.be.true; + expect(lifecycleEmitSpy.called).to.be.true; + expect(lifecycleEmitSpy.args[0][1]).to.deep.equal({ + status: 'fail', + testName: "can't access: https://test.salesforce.com", + }); + }); + + it('fails when one URL cannot be reached and others can be', async () => { + stubMethod(sandbox, Connection.prototype, 'request') + .onFirstCall() + .resolves('{}') + .rejects('{"error":"unsupported_grant_type","error_description":"grant type not supported"}'); + + const dr = Doctor.init(oclifConfig); + const diagnostics = new Diagnostics(dr, oclifConfig); + await diagnostics.networkCheck(); + + expect(drAddSuggestionSpy.called).to.be.true; + expect(lifecycleEmitSpy.called).to.be.true; + expect(lifecycleEmitSpy.args[0][1]).to.deep.equal({ + status: 'pass', + testName: 'can access: https://test.salesforce.com', + }); + expect(lifecycleEmitSpy.args[1][1]).to.deep.equal({ + status: 'fail', + testName: "can't access: https://appexchange.salesforce.com/services/data", + }); + }); + }); + describe('outdatedCliVersionCheck', () => { it('passes when CLI version is equal to latest', async () => { childProcessExecStub.callsFake((cmdString, opts, cb: (e: unknown, stdout: unknown, stderr: unknown) => void) => { diff --git a/yarn.lock b/yarn.lock index c80c6ae9..787c45a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1388,7 +1388,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.0" -"@salesforce/core@^7.3.1", "@salesforce/core@^7.3.3", "@salesforce/core@^7.3.5", "@salesforce/core@^7.3.6", "@salesforce/core@^7.3.8": +"@salesforce/core@^7.3.3", "@salesforce/core@^7.3.5", "@salesforce/core@^7.3.6", "@salesforce/core@^7.3.8": version "7.3.8" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-7.3.8.tgz#8a646b5321f08c0fb4d22e2fa8b1d60b3a20df9b" integrity sha512-VWhXHfjwjtC3pJWYp8wt5/fnNQ5tK61ovMG5eteXzVD2oFd7og1f6YjwuAzoYIZK7kYWWv7KJfGtCsPs7Zw+Ww==