From 5cdaa0bedc05a15ca36d2e106e94c77611b4b6e8 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 14:16:24 -0500 Subject: [PATCH 01/17] refactor: disable ora in tests --- packages/api/src/commands/install.ts | 12 ++++++------ packages/api/src/logger.ts | 9 +++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/api/src/commands/install.ts b/packages/api/src/commands/install.ts index 4ac44094..da76c8e6 100644 --- a/packages/api/src/commands/install.ts +++ b/packages/api/src/commands/install.ts @@ -12,7 +12,7 @@ import { SupportedLanguages, codegenFactory } from '../codegen/factory.js'; import Fetcher from '../fetcher.js'; import promptTerminal from '../lib/prompt.js'; import { buildCodeSnippetForOperation, getSuggestedOperation } from '../lib/suggestedOperations.js'; -import logger from '../logger.js'; +import logger, { oraOptions } from '../logger.js'; import Storage from '../storage.js'; interface Options { @@ -103,7 +103,7 @@ cmd // logger(`It looks like you already have this API installed. Would you like to update it?`); } - let spinner = ora('Fetching your API definition').start(); + let spinner = ora({ text: 'Fetching your API definition', ...oraOptions() }).start(); const storage = new Storage(uri, language); const oas = await storage @@ -136,7 +136,7 @@ cmd await storage.save(oas.api); // @todo look for a prettier config and if we find one ask them if we should use it - spinner = ora('Generating your SDK').start(); + spinner = ora({ text: 'Generating your SDK', ...oraOptions() }).start(); const generator = codegenFactory(language, oas, '../openapi.json', identifier); const sdkSource = await generator .generate() @@ -151,7 +151,7 @@ cmd process.exit(1); }); - spinner = ora('Saving your SDK into your codebase').start(); + spinner = ora({ text: 'Saving your SDK into your codebase', ...oraOptions() }).start(); await storage .saveSourceFiles(sdkSource) .then(() => { @@ -190,7 +190,7 @@ cmd }); } - spinner = ora('Installing required packages').start(); + spinner = ora({ text: 'Installing required packages', ...oraOptions() }).start(); try { await generator.install(storage); spinner.succeed(spinner.text); @@ -202,7 +202,7 @@ cmd } } - spinner = ora('Compiling your SDK').start(); + spinner = ora({ text: 'Compiling your SDK', ...oraOptions() }).start(); try { await generator.compile(storage); spinner.succeed(spinner.text); diff --git a/packages/api/src/logger.ts b/packages/api/src/logger.ts index 544caf0d..128c0f0d 100644 --- a/packages/api/src/logger.ts +++ b/packages/api/src/logger.ts @@ -1,4 +1,6 @@ /* eslint-disable no-console */ +import type { Options as OraOptions } from 'ora'; + import chalk from 'chalk'; export default function logger(log: string, error?: boolean) { @@ -8,3 +10,10 @@ export default function logger(log: string, error?: boolean) { console.log(log); } } + +export function oraOptions() { + // Disables spinner in tests so it doesn't pollute test output + const opts: OraOptions = { isSilent: process.env.NODE_ENV === 'test' }; + + return opts; +} From 491a42a5cba0f5942edee18ccb90dd7e68c225a7 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 14:20:34 -0500 Subject: [PATCH 02/17] refactor: bubble errors up to bin script this will make it easier for us to test! --- packages/api/src/bin.ts | 6 +++++- packages/api/src/commands/install.ts | 21 +++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/api/src/bin.ts b/packages/api/src/bin.ts index 8027e77a..7fb410d6 100644 --- a/packages/api/src/bin.ts +++ b/packages/api/src/bin.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import commands from './commands/index.js'; +import logger from './logger.js'; import * as pkg from './packageInfo.js'; (async () => { @@ -17,5 +18,8 @@ import * as pkg from './packageInfo.js'; program.addCommand(cmd); }); - await program.parseAsync(process.argv); + await program.parseAsync(process.argv).catch(err => { + if (err.message) logger(err.message, true); + process.exit(1); + }); })(); diff --git a/packages/api/src/commands/install.ts b/packages/api/src/commands/install.ts index da76c8e6..1ca50af4 100644 --- a/packages/api/src/commands/install.ts +++ b/packages/api/src/commands/install.ts @@ -120,14 +120,12 @@ cmd .catch(err => { // @todo cleanup installed files spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; }); const identifier = await getIdentifier(oas, uri, options); if (!identifier) { - logger('You must tell us what you would like to identify this API as in order to install it.', true); - process.exit(1); + throw new Error('You must tell us what you would like to identify this API as in order to install it.'); } // Now that we've got an identifier we can save their spec and generate the directory structure @@ -147,8 +145,7 @@ cmd .catch(err => { // @todo cleanup installed files spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; }); spinner = ora({ text: 'Saving your SDK into your codebase', ...oraOptions() }).start(); @@ -160,8 +157,7 @@ cmd .catch(err => { // @todo cleanup installed files spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; }); if (generator.hasRequiredPackages()) { @@ -184,8 +180,7 @@ cmd }).then(({ value }) => { if (!value) { // @todo cleanup installed files - logger('Installation cancelled.', true); - process.exit(1); + throw new Error('Installation cancelled.'); } }); } @@ -197,8 +192,7 @@ cmd } catch (err) { // @todo cleanup installed files spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; } } @@ -209,8 +203,7 @@ cmd } catch (err) { // @todo cleanup installed files spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; } logger(''); From 38e99deee2efe62bbbf3ecbc2b226ed10662ddd7 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 15:41:37 -0500 Subject: [PATCH 03/17] refactor: also don't process.exit in uninstall commands --- packages/api/src/commands/uninstall.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/api/src/commands/uninstall.ts b/packages/api/src/commands/uninstall.ts index d3cacd8b..906c2727 100644 --- a/packages/api/src/commands/uninstall.ts +++ b/packages/api/src/commands/uninstall.ts @@ -26,11 +26,9 @@ cmd const entry = Storage.getFromLockfile(identifier); if (!entry) { - logger( + throw new Error( `You do not appear to have ${identifier} installed. You can run \`npx api list\` to see what SDKs are present.`, - true, ); - process.exit(1); } storage.setLanguage(entry?.language); @@ -47,7 +45,7 @@ cmd initial: true, }).then(({ value }) => { if (!value) { - process.exit(1); + throw new Error('Uninstallation cancelled.'); } }); } @@ -65,8 +63,7 @@ cmd }) .catch(err => { spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; }); } @@ -78,8 +75,7 @@ cmd }) .catch(err => { spinner.fail(spinner.text); - logger(err.message, true); - process.exit(1); + throw err; }); logger('🚀 All done!'); From e17d81b58871569f1fe1c01f7d96b1a4fb3d6006 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 16:29:23 -0500 Subject: [PATCH 04/17] test: first pass at install tests --- packages/api/test/commands/install.test.ts | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/api/test/commands/install.test.ts diff --git a/packages/api/test/commands/install.test.ts b/packages/api/test/commands/install.test.ts new file mode 100644 index 00000000..83340356 --- /dev/null +++ b/packages/api/test/commands/install.test.ts @@ -0,0 +1,73 @@ +import type { SpyInstance } from 'vitest'; + +import { CommanderError } from 'commander'; +import prompts from 'prompts'; +import uniqueTempDir from 'unique-temp-dir'; +import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; + +import installCmd from '../../src/commands/install.js'; +import Storage from '../../src/storage.js'; + +const cmdError = (msg: string) => new CommanderError(0, '', msg); + +const baseCommand = ['api', 'install']; + +describe('install command', () => { + let stdout: string[]; + let stderr: string[]; + let consoleLogSpy: SpyInstance; + + beforeEach(() => { + stdout = []; + stderr = []; + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + installCmd.exitOverride(); + installCmd.configureOutput({ + writeOut: str => stdout.push(str), + writeErr: str => stderr.push(str), + }); + Storage.setStorageDir(uniqueTempDir()); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + Storage.reset(); + }); + + it('should error out if uri is not passed', () => { + return expect(installCmd.parseAsync([...baseCommand])).rejects.toStrictEqual( + cmdError("error: missing required argument 'uri'"), + ); + }); + + it('should error out if invalid uri is passed', () => { + return expect(installCmd.parseAsync([...baseCommand, 'petstore.json'])).rejects.toThrow( + /Sorry, we were unable to load an API definition from .*petstore.json. Please either supply a URL or a path on your filesystem./, + ); + }); + + it('should accept valid lang parameter but error out if invalid uri is passed', () => { + return expect(installCmd.parseAsync([...baseCommand, 'petstore.json', '--lang', 'js'])).rejects.toThrow( + /Sorry, we were unable to load an API definition from .*petstore.json. Please either supply a URL or a path on your filesystem./, + ); + }); + + it('should error out if invalid lang is passed', () => { + return expect(installCmd.parseAsync([...baseCommand, '--lang', 'javascript'])).rejects.toStrictEqual( + cmdError("error: option '-l, --lang ' argument 'javascript' is invalid. Allowed choices are js."), + ); + }); + + it('should handle user answering no to package installation confirmation prompt', () => { + prompts.inject(['petstore', false]); + return expect( + installCmd.parseAsync([...baseCommand, '../test-utils/definitions/simple.json']), + ).rejects.toStrictEqual(new Error('Installation cancelled.')); + }); + + it.todo('should surface generation errors'); + it.todo('should surface file save errors'); + it.todo('should surface package installation errors'); + it.todo('should surface compilation errors'); + it.todo('should successfully generate SDK'); +}); From 11bdc7cb273bf2625bb8487719c83921a9edde92 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 16:30:10 -0500 Subject: [PATCH 05/17] test: first pass at list tests --- packages/api/test/commands/list.test.ts | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/api/test/commands/list.test.ts diff --git a/packages/api/test/commands/list.test.ts b/packages/api/test/commands/list.test.ts new file mode 100644 index 00000000..9cb49ffb --- /dev/null +++ b/packages/api/test/commands/list.test.ts @@ -0,0 +1,44 @@ +import type { SpyInstance } from 'vitest'; + +import uniqueTempDir from 'unique-temp-dir'; +import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; + +import installCmd from '../../src/commands/list.js'; +import Storage from '../../src/storage.js'; + +const baseCommand = ['api', 'list']; + +describe('install command', () => { + let stdout: string[]; + let stderr: string[]; + let consoleLogSpy: SpyInstance; + + const getCommandOutput = () => { + return [consoleLogSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); + }; + + beforeEach(() => { + stdout = []; + stderr = []; + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + installCmd.exitOverride(); + installCmd.configureOutput({ + writeOut: str => stdout.push(str), + writeErr: str => stderr.push(str), + }); + Storage.setStorageDir(uniqueTempDir()); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + Storage.reset(); + }); + + it('should return placeholder message if no SDKs are installed', async () => { + await expect(installCmd.parseAsync([...baseCommand])).resolves.toBeDefined(); + + expect(getCommandOutput()).toBe('😔 You do not have any SDKs installed.'); + }); + + it.todo('should list installed SDKs'); +}); From 0fe05d21c9487a918478fdfd184eb4410b0bdd15 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 17:10:40 -0500 Subject: [PATCH 06/17] refactor: use enum --- packages/api/test/commands/install.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/api/test/commands/install.test.ts b/packages/api/test/commands/install.test.ts index 83340356..99f9109d 100644 --- a/packages/api/test/commands/install.test.ts +++ b/packages/api/test/commands/install.test.ts @@ -7,6 +7,7 @@ import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; import installCmd from '../../src/commands/install.js'; import Storage from '../../src/storage.js'; +import { SupportedLanguages } from '../../src/codegen/factory.js'; const cmdError = (msg: string) => new CommanderError(0, '', msg); @@ -47,7 +48,9 @@ describe('install command', () => { }); it('should accept valid lang parameter but error out if invalid uri is passed', () => { - return expect(installCmd.parseAsync([...baseCommand, 'petstore.json', '--lang', 'js'])).rejects.toThrow( + return expect( + installCmd.parseAsync([...baseCommand, 'petstore.json', '--lang', SupportedLanguages.JS]), + ).rejects.toThrow( /Sorry, we were unable to load an API definition from .*petstore.json. Please either supply a URL or a path on your filesystem./, ); }); From da072a94d2be3cd402746e66716823b130df2e0b Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 17:15:37 -0500 Subject: [PATCH 07/17] test: add another list command test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit we'll see if this snapshot passes in GHA 🤞🏽 --- .../commands/__snapshots__/list.test.ts.snap | 15 ++++++++++++ packages/api/test/commands/list.test.ts | 23 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 packages/api/test/commands/__snapshots__/list.test.ts.snap diff --git a/packages/api/test/commands/__snapshots__/list.test.ts.snap b/packages/api/test/commands/__snapshots__/list.test.ts.snap new file mode 100644 index 00000000..a8fe83ab --- /dev/null +++ b/packages/api/test/commands/__snapshots__/list.test.ts.snap @@ -0,0 +1,15 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`install command > should list installed SDKs 1`] = ` +"petstore + +package name (private): @api/petstore + +language: js + +source: @petstore/v1.0#n6kvf10vakpemvplx + +installer version: 7.0.0-beta.3 + +created at: 2023-10-25T00:00:00.000Z" +`; diff --git a/packages/api/test/commands/list.test.ts b/packages/api/test/commands/list.test.ts index 9cb49ffb..d91c120c 100644 --- a/packages/api/test/commands/list.test.ts +++ b/packages/api/test/commands/list.test.ts @@ -1,8 +1,11 @@ import type { SpyInstance } from 'vitest'; +import { loadSpec } from '@api/test-utils'; +import fetchMock from 'fetch-mock'; import uniqueTempDir from 'unique-temp-dir'; import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; +import { SupportedLanguages } from '../../src/codegen/factory.js'; import installCmd from '../../src/commands/list.js'; import Storage from '../../src/storage.js'; @@ -20,18 +23,22 @@ describe('install command', () => { beforeEach(() => { stdout = []; stderr = []; - consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + installCmd.exitOverride(); installCmd.configureOutput({ writeOut: str => stdout.push(str), writeErr: str => stderr.push(str), }); + + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); Storage.setStorageDir(uniqueTempDir()); + vi.setSystemTime(new Date('2023-10-25')); }); afterEach(() => { consoleLogSpy.mockRestore(); Storage.reset(); + vi.useRealTimers(); }); it('should return placeholder message if no SDKs are installed', async () => { @@ -40,5 +47,17 @@ describe('install command', () => { expect(getCommandOutput()).toBe('😔 You do not have any SDKs installed.'); }); - it.todo('should list installed SDKs'); + it('should list installed SDKs', async () => { + const petstoreSimple = await loadSpec('@readme/oas-examples/3.0/json/petstore-simple.json'); + fetchMock.get('https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx', petstoreSimple); + + const source = '@petstore/v1.0#n6kvf10vakpemvplx'; + const storage = new Storage(source, SupportedLanguages.JS, 'petstore'); + + await storage.load(); + + await expect(installCmd.parseAsync([...baseCommand])).resolves.toBeDefined(); + + expect(getCommandOutput()).toMatchSnapshot(); + }); }); From 41e561117c3933fbbfefa4093500aa2e30d68c1e Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 17:16:24 -0500 Subject: [PATCH 08/17] chore: another test TODO --- packages/api/test/commands/install.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/test/commands/install.test.ts b/packages/api/test/commands/install.test.ts index 99f9109d..1e6fe354 100644 --- a/packages/api/test/commands/install.test.ts +++ b/packages/api/test/commands/install.test.ts @@ -73,4 +73,5 @@ describe('install command', () => { it.todo('should surface package installation errors'); it.todo('should surface compilation errors'); it.todo('should successfully generate SDK'); + it.todo('should successfully bypass all prompts with --yes option'); }); From 438a1a0c4f42bfc82a25094f13388f8069be182f Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 17:18:09 -0500 Subject: [PATCH 09/17] chore: lint --- packages/api/test/commands/install.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/test/commands/install.test.ts b/packages/api/test/commands/install.test.ts index 1e6fe354..e8d06555 100644 --- a/packages/api/test/commands/install.test.ts +++ b/packages/api/test/commands/install.test.ts @@ -5,9 +5,9 @@ import prompts from 'prompts'; import uniqueTempDir from 'unique-temp-dir'; import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; +import { SupportedLanguages } from '../../src/codegen/factory.js'; import installCmd from '../../src/commands/install.js'; import Storage from '../../src/storage.js'; -import { SupportedLanguages } from '../../src/codegen/factory.js'; const cmdError = (msg: string) => new CommanderError(0, '', msg); From b56dc43953e6ba5104183f5ce9aae29a837eb13d Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 17:23:46 -0500 Subject: [PATCH 10/17] test: first pass at uninstall tests --- packages/api/test/commands/uninstall.test.ts | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/api/test/commands/uninstall.test.ts diff --git a/packages/api/test/commands/uninstall.test.ts b/packages/api/test/commands/uninstall.test.ts new file mode 100644 index 00000000..e046a9c8 --- /dev/null +++ b/packages/api/test/commands/uninstall.test.ts @@ -0,0 +1,52 @@ +import type { SpyInstance } from 'vitest'; + +import { CommanderError } from 'commander'; +import uniqueTempDir from 'unique-temp-dir'; +import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; + +import installCmd from '../../src/commands/uninstall.js'; +import Storage from '../../src/storage.js'; + +const cmdError = (msg: string) => new CommanderError(0, '', msg); + +const baseCommand = ['api', 'uninstall']; + +describe('install command', () => { + let stdout: string[]; + let stderr: string[]; + let consoleLogSpy: SpyInstance; + + beforeEach(() => { + stdout = []; + stderr = []; + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + installCmd.exitOverride(); + installCmd.configureOutput({ + writeOut: str => stdout.push(str), + writeErr: str => stderr.push(str), + }); + Storage.setStorageDir(uniqueTempDir()); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + Storage.reset(); + }); + + it('should error out if identifier is not passed', () => { + return expect(installCmd.parseAsync([...baseCommand])).rejects.toStrictEqual( + cmdError("error: missing required argument 'identifier'"), + ); + }); + + it('should error out if invalid identifier is passed', () => { + return expect(installCmd.parseAsync([...baseCommand, 'non-existent-identifier'])).rejects.toStrictEqual( + new Error( + 'You do not appear to have non-existent-identifier installed. You can run `npx api list` to see what SDKs are present.', + ), + ); + }); + + it.todo('should successfully uninstall SDK'); + it.todo('should successfully bypass all prompts with --yes option'); +}); From a0d778a1de0168eb6f21cf56f2cf1409f227a97a Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 17:42:21 -0500 Subject: [PATCH 11/17] test: add snapshot tests for help command --- .../__snapshots__/install.test.ts.snap | 24 +++++++++++++++++++ .../commands/__snapshots__/list.test.ts.snap | 10 ++++++++ .../__snapshots__/uninstall.test.ts.snap | 19 +++++++++++++++ packages/api/test/commands/install.test.ts | 10 ++++++-- packages/api/test/commands/list.test.ts | 9 +++++++ packages/api/test/commands/uninstall.test.ts | 10 ++++++-- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 packages/api/test/commands/__snapshots__/install.test.ts.snap create mode 100644 packages/api/test/commands/__snapshots__/uninstall.test.ts.snap diff --git a/packages/api/test/commands/__snapshots__/install.test.ts.snap b/packages/api/test/commands/__snapshots__/install.test.ts.snap new file mode 100644 index 00000000..ec95a69e --- /dev/null +++ b/packages/api/test/commands/__snapshots__/install.test.ts.snap @@ -0,0 +1,24 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`install command > should print help screen 1`] = ` +"Usage: install [options] + +install an API SDK into your codebase + +Arguments: + uri an API to install + +Options: + -i, --identifier API identifier (eg. \`@api/petstore\`) + -l, --lang SDK language (choices: \\"js\\", default: \\"js\\") + -y, --yes Automatically answer \\"yes\\" to any prompts + printed + -h, --help display help for command + + +Examples: + $ npx api install @developers/v2.0#nysezql0wwo236 + $ npx api install https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/json/petstore-simple.json + $ npx api install ./petstore.json +" +`; diff --git a/packages/api/test/commands/__snapshots__/list.test.ts.snap b/packages/api/test/commands/__snapshots__/list.test.ts.snap index a8fe83ab..589004ce 100644 --- a/packages/api/test/commands/__snapshots__/list.test.ts.snap +++ b/packages/api/test/commands/__snapshots__/list.test.ts.snap @@ -13,3 +13,13 @@ installer version: 7.0.0-beta.3 created at: 2023-10-25T00:00:00.000Z" `; + +exports[`install command > should print help screen 1`] = ` +"Usage: list|ls [options] + +list any installed API SDKs + +Options: + -h, --help display help for command +" +`; diff --git a/packages/api/test/commands/__snapshots__/uninstall.test.ts.snap b/packages/api/test/commands/__snapshots__/uninstall.test.ts.snap new file mode 100644 index 00000000..04fe1257 --- /dev/null +++ b/packages/api/test/commands/__snapshots__/uninstall.test.ts.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`install command > should print help screen 1`] = ` +"Usage: uninstall [options] + +uninstall an SDK from your codebase + +Arguments: + identifier the SDK to uninstall + +Options: + -y, --yes Automatically answer \\"yes\\" to any prompts printed + -h, --help display help for command + + +Examples: + $ npx api uninstall petstore +" +`; diff --git a/packages/api/test/commands/install.test.ts b/packages/api/test/commands/install.test.ts index e8d06555..f4282315 100644 --- a/packages/api/test/commands/install.test.ts +++ b/packages/api/test/commands/install.test.ts @@ -9,10 +9,10 @@ import { SupportedLanguages } from '../../src/codegen/factory.js'; import installCmd from '../../src/commands/install.js'; import Storage from '../../src/storage.js'; -const cmdError = (msg: string) => new CommanderError(0, '', msg); - const baseCommand = ['api', 'install']; +const cmdError = (msg: string) => new CommanderError(0, '', msg); + describe('install command', () => { let stdout: string[]; let stderr: string[]; @@ -68,6 +68,12 @@ describe('install command', () => { ).rejects.toStrictEqual(new Error('Installation cancelled.')); }); + it('should print help screen', async () => { + await expect(installCmd.parseAsync([...baseCommand, '--help'])).rejects.toStrictEqual(cmdError('(outputHelp)')); + + expect(stdout.join('\n')).toMatchSnapshot(); + }); + it.todo('should surface generation errors'); it.todo('should surface file save errors'); it.todo('should surface package installation errors'); diff --git a/packages/api/test/commands/list.test.ts b/packages/api/test/commands/list.test.ts index d91c120c..cab57c11 100644 --- a/packages/api/test/commands/list.test.ts +++ b/packages/api/test/commands/list.test.ts @@ -1,6 +1,7 @@ import type { SpyInstance } from 'vitest'; import { loadSpec } from '@api/test-utils'; +import { CommanderError } from 'commander'; import fetchMock from 'fetch-mock'; import uniqueTempDir from 'unique-temp-dir'; import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; @@ -11,6 +12,8 @@ import Storage from '../../src/storage.js'; const baseCommand = ['api', 'list']; +const cmdError = (msg: string) => new CommanderError(0, '', msg); + describe('install command', () => { let stdout: string[]; let stderr: string[]; @@ -60,4 +63,10 @@ describe('install command', () => { expect(getCommandOutput()).toMatchSnapshot(); }); + + it('should print help screen', async () => { + await expect(installCmd.parseAsync([...baseCommand, '--help'])).rejects.toStrictEqual(cmdError('(outputHelp)')); + + expect(stdout.join('\n')).toMatchSnapshot(); + }); }); diff --git a/packages/api/test/commands/uninstall.test.ts b/packages/api/test/commands/uninstall.test.ts index e046a9c8..d295e298 100644 --- a/packages/api/test/commands/uninstall.test.ts +++ b/packages/api/test/commands/uninstall.test.ts @@ -7,10 +7,10 @@ import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; import installCmd from '../../src/commands/uninstall.js'; import Storage from '../../src/storage.js'; -const cmdError = (msg: string) => new CommanderError(0, '', msg); - const baseCommand = ['api', 'uninstall']; +const cmdError = (msg: string) => new CommanderError(0, '', msg); + describe('install command', () => { let stdout: string[]; let stderr: string[]; @@ -47,6 +47,12 @@ describe('install command', () => { ); }); + it('should print help screen', async () => { + await expect(installCmd.parseAsync([...baseCommand, '--help'])).rejects.toStrictEqual(cmdError('(outputHelp)')); + + expect(stdout.join('\n')).toMatchSnapshot(); + }); + it.todo('should successfully uninstall SDK'); it.todo('should successfully bypass all prompts with --yes option'); }); From 1dc0b564cc39dfb4e28fdd9b46ceb8a763d3af74 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 18:16:00 -0500 Subject: [PATCH 12/17] refactor: use ora opts in uninstall command --- packages/api/src/commands/uninstall.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api/src/commands/uninstall.ts b/packages/api/src/commands/uninstall.ts index 906c2727..08eddf27 100644 --- a/packages/api/src/commands/uninstall.ts +++ b/packages/api/src/commands/uninstall.ts @@ -6,7 +6,7 @@ import ora from 'ora'; import { SupportedLanguages, uninstallerFactory } from '../codegen/factory.js'; import promptTerminal from '../lib/prompt.js'; -import logger from '../logger.js'; +import logger, { oraOptions } from '../logger.js'; import Storage from '../storage.js'; interface Options { @@ -50,7 +50,7 @@ cmd }); } - let spinner = ora(`Uninstalling ${chalk.grey(identifier)}`).start(); + let spinner = ora({ text: `Uninstalling ${chalk.grey(identifier)}`, ...oraOptions() }).start(); // If we have a known package name for this then we can uninstall it from within cooresponding // package manager. @@ -67,7 +67,7 @@ cmd }); } - spinner = ora(`Removing ${chalk.grey(directory)}`).start(); + spinner = ora({ text: `Removing ${chalk.grey(directory)}`, ...oraOptions() }).start(); await storage .remove() .then(() => { From 3418fcca717141fb64d76e9099beb48f65644971 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 18:16:23 -0500 Subject: [PATCH 13/17] test(list): make sure fetchmock is restored --- packages/api/test/commands/list.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/test/commands/list.test.ts b/packages/api/test/commands/list.test.ts index cab57c11..0e780c74 100644 --- a/packages/api/test/commands/list.test.ts +++ b/packages/api/test/commands/list.test.ts @@ -40,6 +40,7 @@ describe('install command', () => { afterEach(() => { consoleLogSpy.mockRestore(); + fetchMock.restore(); Storage.reset(); vi.useRealTimers(); }); From 0a75a2a82ec99a353c550c938b57ebc609b73e6f Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 18:17:42 -0500 Subject: [PATCH 14/17] test: more uninstall command tests --- packages/api/test/commands/uninstall.test.ts | 55 ++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/api/test/commands/uninstall.test.ts b/packages/api/test/commands/uninstall.test.ts index d295e298..502387bd 100644 --- a/packages/api/test/commands/uninstall.test.ts +++ b/packages/api/test/commands/uninstall.test.ts @@ -1,9 +1,14 @@ import type { SpyInstance } from 'vitest'; +import { loadSpec } from '@api/test-utils'; import { CommanderError } from 'commander'; +import fetchMock from 'fetch-mock'; +import prompts from 'prompts'; import uniqueTempDir from 'unique-temp-dir'; import { describe, beforeEach, it, expect, vi, afterEach } from 'vitest'; +import { SupportedLanguages } from '../../src/codegen/factory.js'; +import * as codegenFactoryModule from '../../src/codegen/factory.js'; import installCmd from '../../src/commands/uninstall.js'; import Storage from '../../src/storage.js'; @@ -16,6 +21,10 @@ describe('install command', () => { let stderr: string[]; let consoleLogSpy: SpyInstance; + const getCommandOutput = () => { + return [consoleLogSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); + }; + beforeEach(() => { stdout = []; stderr = []; @@ -29,8 +38,9 @@ describe('install command', () => { }); afterEach(() => { - consoleLogSpy.mockRestore(); + fetchMock.restore(); Storage.reset(); + vi.restoreAllMocks(); }); it('should error out if identifier is not passed', () => { @@ -53,6 +63,45 @@ describe('install command', () => { expect(stdout.join('\n')).toMatchSnapshot(); }); - it.todo('should successfully uninstall SDK'); - it.todo('should successfully bypass all prompts with --yes option'); + it('should successfully uninstall SDK', async () => { + prompts.inject([true]); + + const petstoreSimple = await loadSpec('@readme/oas-examples/3.0/json/petstore-simple.json'); + fetchMock.get('https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx', petstoreSimple); + + const source = '@petstore/v1.0#n6kvf10vakpemvplx'; + const identifier = 'petstore-to-be-uninstalled'; + const storage = new Storage(source, SupportedLanguages.JS, identifier); + + await storage.load(); + + expect(Storage.getLockfile().apis).toHaveLength(1); + + const uninstallSpy = vi.spyOn(codegenFactoryModule, 'uninstallerFactory').mockResolvedValue(); + + await expect(installCmd.parseAsync([...baseCommand, identifier])).resolves.toBeDefined(); + expect(uninstallSpy).toHaveBeenCalledTimes(1); + expect(getCommandOutput()).toBe('🚀 All done!'); + expect(Storage.getLockfile().apis).toHaveLength(0); + }); + + it('should uninstall SDK and bypass prompt with --yes option', async () => { + const petstoreSimple = await loadSpec('@readme/oas-examples/3.0/json/petstore-simple.json'); + fetchMock.get('https://dash.readme.com/api/v1/api-registry/n6kvf10vakpemvplx', petstoreSimple); + + const source = '@petstore/v1.0#n6kvf10vakpemvplx'; + const identifier = 'petstore-to-be-uninstalled'; + const storage = new Storage(source, SupportedLanguages.JS, identifier); + + await storage.load(); + + expect(Storage.getLockfile().apis).toHaveLength(1); + + const uninstallSpy = vi.spyOn(codegenFactoryModule, 'uninstallerFactory').mockResolvedValue(); + + await expect(installCmd.parseAsync([...baseCommand, identifier, '--yes'])).resolves.toBeDefined(); + expect(uninstallSpy).toHaveBeenCalledTimes(1); + expect(getCommandOutput()).toBe('🚀 All done!'); + expect(Storage.getLockfile().apis).toHaveLength(0); + }); }); From 835fb6ed4ce73cdd86b60f82caaab3f2adbd5fd3 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 18:22:14 -0500 Subject: [PATCH 15/17] chore: consistent whitespace --- packages/api/test/commands/install.test.ts | 4 +++- packages/api/test/commands/uninstall.test.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/api/test/commands/install.test.ts b/packages/api/test/commands/install.test.ts index f4282315..b8a3c1f6 100644 --- a/packages/api/test/commands/install.test.ts +++ b/packages/api/test/commands/install.test.ts @@ -21,12 +21,14 @@ describe('install command', () => { beforeEach(() => { stdout = []; stderr = []; - consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + installCmd.exitOverride(); installCmd.configureOutput({ writeOut: str => stdout.push(str), writeErr: str => stderr.push(str), }); + + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); Storage.setStorageDir(uniqueTempDir()); }); diff --git a/packages/api/test/commands/uninstall.test.ts b/packages/api/test/commands/uninstall.test.ts index 502387bd..03a86499 100644 --- a/packages/api/test/commands/uninstall.test.ts +++ b/packages/api/test/commands/uninstall.test.ts @@ -28,12 +28,14 @@ describe('install command', () => { beforeEach(() => { stdout = []; stderr = []; - consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + installCmd.exitOverride(); installCmd.configureOutput({ writeOut: str => stdout.push(str), writeErr: str => stderr.push(str), }); + + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); Storage.setStorageDir(uniqueTempDir()); }); From b0e888c82ed17b5d73596af7076fb1442873f462 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Mon, 30 Oct 2023 19:11:41 -0500 Subject: [PATCH 16/17] refactor: use override in uninstall command --- packages/api/src/commands/uninstall.ts | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/api/src/commands/uninstall.ts b/packages/api/src/commands/uninstall.ts index 08eddf27..daf5331e 100644 --- a/packages/api/src/commands/uninstall.ts +++ b/packages/api/src/commands/uninstall.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import chalk from 'chalk'; import { Command, Option } from 'commander'; import ora from 'ora'; +import prompts from 'prompts'; import { SupportedLanguages, uninstallerFactory } from '../codegen/factory.js'; import promptTerminal from '../lib/prompt.js'; @@ -35,20 +36,21 @@ cmd storage.setIdentifier(identifier); const directory = path.relative(process.cwd(), storage.getIdentifierStorageDir()); - if (!options.yes) { - await promptTerminal({ - type: 'confirm', - name: 'value', - message: `Are you sure you want to uninstall ${chalk.yellow(identifier)}? This will delete the ${chalk.yellow( - directory, - )} directory and potentially any changes you may have made there.`, - initial: true, - }).then(({ value }) => { - if (!value) { - throw new Error('Uninstallation cancelled.'); - } - }); - } + + // funnels `--yes` option into prompt + prompts.override(options); + await promptTerminal({ + type: 'confirm', + name: 'yes', + message: `Are you sure you want to uninstall ${chalk.yellow(identifier)}? This will delete the ${chalk.yellow( + directory, + )} directory and potentially any changes you may have made there.`, + initial: true, + }).then(({ yes }) => { + if (!yes) { + throw new Error('Uninstallation cancelled.'); + } + }); let spinner = ora({ text: `Uninstalling ${chalk.grey(identifier)}`, ...oraOptions() }).start(); From 5b46105a83c669d4ca586d5352cf277352508274 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Tue, 31 Oct 2023 12:46:23 -0500 Subject: [PATCH 17/17] refactor: prompt.override in install command --- packages/api/src/commands/install.ts | 67 ++++++++++++---------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/packages/api/src/commands/install.ts b/packages/api/src/commands/install.ts index 1ca50af4..d6b318b9 100644 --- a/packages/api/src/commands/install.ts +++ b/packages/api/src/commands/install.ts @@ -6,6 +6,7 @@ import { emphasize } from 'emphasize'; import figures from 'figures'; import Oas from 'oas'; import ora from 'ora'; +import prompts from 'prompts'; import uslug from 'uslug'; import { SupportedLanguages, codegenFactory } from '../codegen/factory.js'; @@ -21,36 +22,26 @@ interface Options { yes?: boolean; } -async function getLanguage(options: Options) { - let language: SupportedLanguage; - if (options.lang) { - language = options.lang; - } else { - ({ value: language } = await promptTerminal({ - type: 'select', - name: 'value', - message: 'What language would you like to generate an SDK for?', - choices: [{ title: 'JavaScript', value: SupportedLanguages.JS }], - initial: 1, - })); - } +async function getLanguage() { + const { lang }: { lang: SupportedLanguage } = await promptTerminal({ + type: 'select', + name: 'lang', + message: 'What language would you like to generate an SDK for?', + choices: [{ title: 'JavaScript', value: SupportedLanguages.JS }], + initial: 1, + }); - return language; + return lang; } -async function getIdentifier(oas: Oas, uri: string, options: Options) { +async function getIdentifier(oas: Oas, uri: string) { let identifier; - if (options.identifier) { - // `Storage.isIdentifierValid` will throw an exception if an identifier is invalid. - if (Storage.isIdentifierValid(options.identifier)) { - identifier = options.identifier; - } - } else if (Fetcher.isAPIRegistryUUID(uri)) { + if (Fetcher.isAPIRegistryUUID(uri)) { identifier = Fetcher.getProjectPrefixFromRegistryUUID(uri); } else { - ({ value: identifier } = await promptTerminal({ + ({ identifier } = await promptTerminal({ type: 'text', - name: 'value', + name: 'identifier', initial: oas.api?.info?.title ? uslug(oas.api.info.title, { lower: true }) : undefined, message: 'What would you like to identify this API as? This will be how you use the SDK. (e.g. entering `petstore` would result in `@api/petstore`)', @@ -92,7 +83,9 @@ cmd ) .addOption(new Option('-y, --yes', 'Automatically answer "yes" to any prompts printed')) .action(async (uri: string, options: Options) => { - const language = await getLanguage(options); + prompts.override(options); + + const language = await getLanguage(); // @todo let them know that we're going to be creating a `.api/ directory // @todo detect if they have a gitigore and .npmignore and if .api woudl be ignored by that @@ -123,7 +116,7 @@ cmd throw err; }); - const identifier = await getIdentifier(oas, uri, options); + const identifier = await getIdentifier(oas, uri); if (!identifier) { throw new Error('You must tell us what you would like to identify this API as in order to install it.'); } @@ -171,19 +164,17 @@ cmd logger(msg); }); - if (!options.yes) { - await promptTerminal({ - type: 'confirm', - name: 'value', - message: 'OK to proceed with package installation?', - initial: true, - }).then(({ value }) => { - if (!value) { - // @todo cleanup installed files - throw new Error('Installation cancelled.'); - } - }); - } + await promptTerminal({ + type: 'confirm', + name: 'yes', + message: 'OK to proceed with package installation?', + initial: true, + }).then(({ yes }) => { + if (!yes) { + // @todo cleanup installed files + throw new Error('Installation cancelled.'); + } + }); spinner = ora({ text: 'Installing required packages', ...oraOptions() }).start(); try {