diff --git a/src/cli.ts b/src/cli.ts index 89c8028fc..46a2d9175 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,7 +6,7 @@ import { VSCodeSettings } from '@redhat-developer/vscode-redhat-telemetry/lib/common/vscode/settings'; import * as cp from 'child_process'; import { CommandText } from './base/command'; -import { ToolsConfig } from './tools'; +import { ToolNotFoundError, ToolsConfig } from './tools'; import { ChildProcessUtil, CliExitData } from './util/childProcessUtil'; import { ExecutionContext } from './util/utils'; import { VsCommandError } from './vscommand'; @@ -128,6 +128,9 @@ export class CliChannel { } const toolLocation = await ToolsConfig.detect(cmd.command); + if (!toolLocation) { + throw new ToolNotFoundError(cmd.command); + } const optWithTelemetryEnv = CliChannel.applyEnv(options, CliChannel.createTelemetryEnv()) as cp.ExecFileSyncOptionsWithStringEncoding; const result: string = cp.execFileSync(toolLocation, [cmd.parameter, ...cmd.options.map((o)=>o.toString())], optWithTelemetryEnv); diff --git a/src/tools.ts b/src/tools.ts index 8cd43fa0c..84c7b2abc 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -13,6 +13,13 @@ import configData from './tools.json'; import { ChildProcessUtil } from './util/childProcessUtil'; import { Platform } from './util/platform'; +export class ToolNotFoundError extends Error { + constructor(public readonly toolName: string) { + super(`'${toolName}' tool not found. Install it or enable 'openshiftToolkit.searchForToolsInPath' setting.`); + this.name = 'ToolNotFoundError'; + } +} + export class ToolsConfig { public static tools: any = ToolsConfig.loadMetadata(configData, Platform.OS); diff --git a/src/util/loginUtil.ts b/src/util/loginUtil.ts index 91b729164..63173b4be 100644 --- a/src/util/loginUtil.ts +++ b/src/util/loginUtil.ts @@ -6,6 +6,7 @@ import { CommandOption, CommandText } from '../base/command'; import { CliChannel } from '../cli'; import { Oc } from '../oc/ocWrapper'; +import { ToolNotFoundError } from '../tools'; import { isOpenShiftCluster } from './kubeUtils'; export class LoginUtil { @@ -37,9 +38,15 @@ export class LoginUtil { return await CliChannel.getInstance().executeSyncTool( new CommandText('oc', 'api-versions'), { timeout: 5000 }) .then((response) => !response || response.trim().length === 0) // Active user is set - no need to login - .catch((error) => true); + .catch((error) => { + if (error instanceof ToolNotFoundError) throw error; + return true; + }); }) - .catch((error) => true); // Can't get server - require to login + .catch((error) => { + if (error instanceof ToolNotFoundError) throw error; + return true; + }); } /** diff --git a/test/unit/tools.test.ts b/test/unit/tools.test.ts index 5bd017119..79064f9e6 100644 --- a/test/unit/tools.test.ts +++ b/test/unit/tools.test.ts @@ -15,6 +15,7 @@ import sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; import { ChildProcessUtil, CliExitData } from '../../src/util/childProcessUtil'; import { Platform } from '../../src/util/platform'; +import { ToolNotFoundError } from '../../src/tools'; import * as utils from '../../src/util/utils'; chai.use(sinonChai); @@ -152,4 +153,28 @@ suite('tools configuration', () => { assert.ok(!config.odo); }); }); + + suite('ToolNotFoundError', () => { + test('extends Error', () => { + const error = new ToolNotFoundError('oc'); + assert.ok(error instanceof Error); + assert.ok(error instanceof ToolNotFoundError); + }); + + test('has correct name property', () => { + const error = new ToolNotFoundError('oc'); + assert.equal(error.name, 'ToolNotFoundError'); + }); + + test('stores toolName', () => { + const error = new ToolNotFoundError('oc'); + assert.equal(error.toolName, 'oc'); + }); + + test('message includes tool name and setting hint', () => { + const error = new ToolNotFoundError('oc'); + assert.ok(error.message.includes('\'oc\'')); + assert.ok(error.message.includes('searchForToolsInPath')); + }); + }); }); diff --git a/test/unit/util/loginUtil.test.ts b/test/unit/util/loginUtil.test.ts new file mode 100644 index 000000000..89f049741 --- /dev/null +++ b/test/unit/util/loginUtil.test.ts @@ -0,0 +1,84 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { CliChannel } from '../../../src/cli'; +import { ToolNotFoundError } from '../../../src/tools'; +import { LoginUtil } from '../../../src/util/loginUtil'; + +const { expect } = chai; +chai.use(sinonChai); + +suite('util/loginUtil.ts', () => { + let sandbox: sinon.SinonSandbox; + let executeSyncToolStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + executeSyncToolStub = sandbox.stub(CliChannel.getInstance(), 'executeSyncTool'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('requireLogin()', () => { + test('returns false when oc whoami and api-versions succeed', async () => { + executeSyncToolStub.onFirstCall().resolves('https://api.cluster:6443\n'); + executeSyncToolStub.onSecondCall().resolves('apps/v1\nv1\n'); + + const result = await LoginUtil.Instance.requireLogin(); + expect(result).to.be.false; + }); + + test('returns true when oc whoami fails with regular error', async () => { + executeSyncToolStub.rejects(new Error('connection refused')); + + const result = await LoginUtil.Instance.requireLogin(); + expect(result).to.be.true; + }); + + test('returns true when api-versions returns empty', async () => { + executeSyncToolStub.onFirstCall().resolves('https://api.cluster:6443\n'); + executeSyncToolStub.onSecondCall().resolves(''); + + const result = await LoginUtil.Instance.requireLogin(); + expect(result).to.be.true; + }); + + test('propagates ToolNotFoundError from oc whoami', async () => { + executeSyncToolStub.rejects(new ToolNotFoundError('oc')); + + try { + await LoginUtil.Instance.requireLogin(); + expect.fail('should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(ToolNotFoundError); + expect((error as ToolNotFoundError).toolName).to.equal('oc'); + } + }); + + test('propagates ToolNotFoundError from api-versions', async () => { + executeSyncToolStub.onFirstCall().resolves('https://api.cluster:6443\n'); + executeSyncToolStub.onSecondCall().rejects(new ToolNotFoundError('oc')); + + try { + await LoginUtil.Instance.requireLogin(); + expect.fail('should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(ToolNotFoundError); + } + }); + + test('returns true when server URI does not match', async () => { + executeSyncToolStub.onFirstCall().resolves('https://api.cluster:6443\n'); + + const result = await LoginUtil.Instance.requireLogin('https://other.cluster:6443'); + expect(result).to.be.true; + }); + }); +});