diff --git a/src/extension.ts b/src/extension.ts index 0c1f56e3..52038ab8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -33,7 +33,9 @@ export function activate(this: any, context: ExtensionContext) { Resource.initialize(context); - const stripeCommands = new Commands(telemetry, new StripeTerminal()); + const stripeTerminal = new StripeTerminal(stripeClient); + + const stripeCommands = new Commands(telemetry, stripeTerminal); // Activity bar view window.createTreeView('stripeDashboardView', { diff --git a/src/stripeClient.ts b/src/stripeClient.ts index ee593b17..c19e60a3 100644 --- a/src/stripeClient.ts +++ b/src/stripeClient.ts @@ -10,7 +10,7 @@ const fs = require('fs'); export class StripeClient { telemetry: Telemetry; - cliPath: string | null; + private cliPath: string | null; constructor(telemetry:Telemetry) { this.telemetry = telemetry; @@ -98,7 +98,25 @@ export class StripeClient { } } - async detectInstalled() { + getEvents() { + const events = this.execute('events list'); + return events; + } + + getResourceById(id: string) { + const resource = this.execute(`get ${id}`); + return resource; + } + + async getCLIPath(): Promise { + const isInstalled = await this.detectInstalled(); + if (!isInstalled) { + return null; + } + return this.cliPath; + } + + private async detectInstalled() { const defaultInstallPath = (() => { const osType: OSType = getOSType(); switch (osType) { @@ -140,16 +158,6 @@ export class StripeClient { return false; } - getEvents() { - const events = this.execute('events list'); - return events; - } - - getResourceById(id: string) { - const resource = this.execute(`get ${id}`); - return resource; - } - private async handleDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { const shouldHandleConfigurationChange = e.affectsConfiguration('stripe'); if (shouldHandleConfigurationChange) { diff --git a/src/stripeTerminal.ts b/src/stripeTerminal.ts index bc29fedd..59d0932a 100644 --- a/src/stripeTerminal.ts +++ b/src/stripeTerminal.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import {OSType, filterAsync, findAsync, getOSType} from './utils'; +import {StripeClient} from './stripeClient'; import psList from 'ps-list'; type SupportedStripeCommand = 'events' | 'listen' | 'logs' | 'login' | 'trigger'; @@ -7,13 +8,15 @@ type SupportedStripeCommand = 'events' | 'listen' | 'logs' | 'login' | 'trigger' export class StripeTerminal { // eslint-disable-next-line @typescript-eslint/naming-convention private static KNOWN_LONG_RUNNING_COMMANDS = [ - 'stripe listen', - 'stripe logs tail', + 'listen', + 'logs tail', ]; + private stripeClient: StripeClient; private terminals: Array; - constructor() { + constructor(stripeClient: StripeClient) { + this.stripeClient = stripeClient; this.terminals = []; vscode.window.onDidCloseTerminal((terminal) => { terminal.dispose(); @@ -25,17 +28,22 @@ export class StripeTerminal { command: SupportedStripeCommand, args: Array = [], ): Promise { + const cliPath = await this.stripeClient.getCLIPath(); + if (!cliPath) { + return; + } + const globalCLIFLags = this.getGlobalCLIFlags(); const commandString = [ - 'stripe', + cliPath, command, ...args, ...globalCLIFLags ].join(' '); const allRunningProcesses = await psList(); - const terminal = await this.terminalForCommand(commandString, allRunningProcesses); + const terminal = await this.terminalForCommand(commandString, cliPath, allRunningProcesses); terminal.sendText(commandString); terminal.show(); const otherTerminals = this.terminals.filter((t) => t !== terminal); @@ -56,8 +64,8 @@ export class StripeTerminal { private isCommandLongRunning(command: string): boolean { if (getOSType() === OSType.windows) { - // On Windows we can't get the process command, so always assume terminals running `stripe` are long running - return command.indexOf('stripe') > -1; + // On Windows we can't get the process command, so always assume commands are long-running + return true; } return StripeTerminal.KNOWN_LONG_RUNNING_COMMANDS.some((knownCommand) => ( command.indexOf(knownCommand) > -1 @@ -92,11 +100,14 @@ export class StripeTerminal { private async terminalForCommand( command: string, + cliPath: string, allRunningProcesses: psList.ProcessDescriptor[], ): Promise { + const isStripeCLICommand = command.startsWith(cliPath); + // If the command is a long-running one, and it's already running in a VS Code terminal, // we restart it in the same terminal. This does not occur on Windows due to OS limitations. - if (this.isCommandLongRunning(command)) { + if (isStripeCLICommand && this.isCommandLongRunning(command)) { const terminalWithDesiredCommand = await findAsync(this.terminals, async (t) => { const runningCommand = await this.getRunningCommand(t, allRunningProcesses); return runningCommand === command; diff --git a/src/test/suite/stripeClient.test.ts b/src/test/suite/stripeClient.test.ts index 88cf6f15..8eb613bb 100644 --- a/src/test/suite/stripeClient.test.ts +++ b/src/test/suite/stripeClient.test.ts @@ -18,7 +18,7 @@ suite('stripeClient', () => { sandbox.restore(); }); - suite('detectInstalled', () => { + suite('getCLIPath', () => { suite('with default CLI install path', () => { const osPathPairs: [utils.OSType, string][] = [ [utils.OSType.linux, '/usr/local/bin/stripe'], @@ -44,21 +44,19 @@ suite('stripeClient', () => { test('detects installed', async () => { statStub.returns(Promise.resolve({isFile: () => true})); // the path is a file; CLI found const stripeClient = new StripeClient(new NoOpTelemetry()); - const isInstalled = await stripeClient.detectInstalled(); - assert.strictEqual(isInstalled, true); + const cliPath = await stripeClient.getCLIPath(); + assert.strictEqual(cliPath, path); assert.deepStrictEqual(realpathStub.args[0], [path]); assert.deepStrictEqual(statStub.args[0], [resolvedPath]); - assert.strictEqual(stripeClient.cliPath, path); }); test('detects not installed', async () => { statStub.returns(Promise.resolve({isFile: () => false})); // the path is not a file; CLI not found const stripeClient = new StripeClient(new NoOpTelemetry()); - const isInstalled = await stripeClient.detectInstalled(); - assert.strictEqual(isInstalled, false); + const cliPath = await stripeClient.getCLIPath(); + assert.strictEqual(cliPath, null); assert.deepStrictEqual(realpathStub.args[0], [path]); assert.deepStrictEqual(statStub.args[0], [resolvedPath]); - assert.strictEqual(stripeClient.cliPath, null); }); }); }); @@ -67,8 +65,8 @@ suite('stripeClient', () => { sandbox.stub(fs.promises, 'stat').returns(Promise.resolve({isFile: () => false})); const showErrorMessageSpy = sandbox.stub(vscode.window, 'showErrorMessage'); const stripeClient = new StripeClient(new NoOpTelemetry()); - const isInstalled = await stripeClient.detectInstalled(); - assert.strictEqual(isInstalled, false); + const cliPath = await stripeClient.getCLIPath(); + assert.strictEqual(cliPath, null); assert.deepStrictEqual( showErrorMessageSpy.args[0], [ @@ -108,21 +106,19 @@ suite('stripeClient', () => { test('detects installed', async () => { statStub.returns(Promise.resolve({isFile: () => true})); // the path is a file; CLI found const stripeClient = new StripeClient(new NoOpTelemetry()); - const isInstalled = await stripeClient.detectInstalled(); - assert.strictEqual(isInstalled, true); + const cliPath = await stripeClient.getCLIPath(); + assert.strictEqual(cliPath, customPath); assert.deepStrictEqual(realpathStub.args[0], [customPath]); assert.deepStrictEqual(statStub.args[0], [resolvedPath]); - assert.strictEqual(stripeClient.cliPath, customPath); }); test('detects not installed', async () => { statStub.returns(Promise.resolve({isFile: () => false})); // the path is not a file; CLI not found const stripeClient = new StripeClient(new NoOpTelemetry()); - const isInstalled = await stripeClient.detectInstalled(); - assert.strictEqual(isInstalled, false); + const cliPath = await stripeClient.getCLIPath(); + assert.strictEqual(cliPath, null); assert.deepStrictEqual(realpathStub.args[0], [customPath]); assert.deepStrictEqual(statStub.args[0], [resolvedPath]); - assert.strictEqual(stripeClient.cliPath, null); }); }); }); @@ -131,8 +127,8 @@ suite('stripeClient', () => { statStub.returns(Promise.resolve({isFile: () => false})); const showErrorMessageSpy = sandbox.stub(vscode.window, 'showErrorMessage'); const stripeClient = new StripeClient(new NoOpTelemetry()); - const isInstalled = await stripeClient.detectInstalled(); - assert.strictEqual(isInstalled, false); + const cliPath = await stripeClient.getCLIPath(); + assert.strictEqual(cliPath, null); assert.deepStrictEqual( showErrorMessageSpy.args[0], ["You set a custom installation path for the Stripe CLI, but we couldn't find the executable in '/foo/bar/baz'", 'Ok'], diff --git a/src/test/suite/stripeTerminal.test.ts b/src/test/suite/stripeTerminal.test.ts index 36667190..2aedae45 100644 --- a/src/test/suite/stripeTerminal.test.ts +++ b/src/test/suite/stripeTerminal.test.ts @@ -27,14 +27,59 @@ suite('stripeTerminal', function() { sandbox.restore(); }); - test('sends command to a new terminal if no terminals exist', async () => { - const sendTextStub = sandbox.stub(terminalStub, 'sendText'); - const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub); + [ + '/usr/local/bin/stripe', + '/custom/path/to/stripe' + ].forEach((path) => { + suite(`when the Stripe CLI is installed at ${path}`, () => { + test(`runs command with ${path}`, async () => { + const sendTextStub = sandbox.stub(terminalStub, 'sendText'); + const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub); + const stripeClientStub = {getCLIPath: () => {}}; + const getCLIPathStub = sandbox.stub(stripeClientStub, 'getCLIPath').returns(Promise.resolve(path)); - const stripeTerminal = new StripeTerminal(); - await stripeTerminal.execute('listen', ['--foward-to', 'localhost']); + const stripeTerminal = new StripeTerminal(stripeClientStub); + await stripeTerminal.execute('listen', ['--forward-to', 'localhost']); - assert.strictEqual(createTerminalStub.callCount, 1); - assert.strictEqual(sendTextStub.getCalls()[0].args[0], 'stripe listen --foward-to localhost'); + assert.strictEqual(getCLIPathStub.callCount, 1); + assert.strictEqual(createTerminalStub.callCount, 1); + assert.deepStrictEqual(sendTextStub.args[0], [`${path} listen --forward-to localhost`]); + }); + }); + }); + + suite('with no Stripe CLI installed', () => { + test('does not run command', async () => { + const sendTextStub = sandbox.stub(terminalStub, 'sendText'); + const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub); + const stripeClientStub = {getCLIPath: () => {}}; + sandbox.stub(stripeClientStub, 'getCLIPath').returns(null); + + const stripeTerminal = new StripeTerminal(stripeClientStub); + await stripeTerminal.execute('listen', ['--forward-to', 'localhost']); + + assert.strictEqual(createTerminalStub.callCount, 0); + assert.deepStrictEqual(sendTextStub.callCount, 0); + }); + }); + + suite('if a Stripe terminal already exists', () => { + test('reuses terminal if the command is the same', async () => { + const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns(terminalStub); + const sendTextStub = sandbox.stub(terminalStub, 'sendText'); + const stripeClientStub = {getCLIPath: () => {}}; + sandbox.stub(stripeClientStub, 'getCLIPath') + .returns(Promise.resolve('/usr/local/bin/stripe')); + + const stripeTerminal = new StripeTerminal(stripeClientStub); + await stripeTerminal.execute('listen', ['--forward-to', 'localhost']); + + // same command => reuse the same terminal + await stripeTerminal.execute('listen', ['--forward-to', 'localhost']); + + assert.strictEqual(createTerminalStub.callCount, 1); + assert.deepStrictEqual(sendTextStub.args[0], ['/usr/local/bin/stripe listen --forward-to localhost']); + assert.deepStrictEqual(sendTextStub.args[1], ['/usr/local/bin/stripe listen --forward-to localhost']); + }); }); });