diff --git a/__tests__/commands/__snapshots__/login.test.ts.snap b/__tests__/commands/__snapshots__/login.test.ts.snap new file mode 100644 index 000000000..8c2112941 --- /dev/null +++ b/__tests__/commands/__snapshots__/login.test.ts.snap @@ -0,0 +1,77 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`rdme login > should bypass prompts and post to /login on the API if passing in every opt (no 2FA) 1`] = ` +{ + "result": "Successfully logged in as user@example.com to the subdomain project.", + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should bypass prompts and post to /login on the API if passing in every opt 1`] = ` +{ + "result": "Successfully logged in as user@example.com to the subdomain project.", + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should error if email is invalid 1`] = ` +{ + "error": [Error: You must provide a valid email address.], + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should error if invalid credentials are given 1`] = ` +{ + "error": [APIv1Error: Either your email address or password is incorrect + +If you need help, email support@readme.io and mention log "fake-metrics-uuid".], + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should error if no project provided 1`] = ` +{ + "error": [Error: No project subdomain provided. Please use \`--project\`.], + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should error if trying to access a project that is not yours 1`] = ` +{ + "error": [APIv1Error: The project (unauthorized-project) can't be found. + +If you need help, email support@readme.io], + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should make additional prompt for token if login requires 2FA 1`] = ` +{ + "result": "Successfully logged in as user@example.com to the subdomain project.", + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should post to /login on the API 1`] = ` +{ + "result": "Successfully logged in as user@example.com to the subdomain project.", + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme login > should post to /login on the API if passing in project via opt 1`] = ` +{ + "result": "Successfully logged in as user@example.com to the subdomain project.", + "stderr": "", + "stdout": "", +} +`; diff --git a/__tests__/commands/__snapshots__/logout.test.ts.snap b/__tests__/commands/__snapshots__/logout.test.ts.snap new file mode 100644 index 000000000..a7756dc5d --- /dev/null +++ b/__tests__/commands/__snapshots__/logout.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`rdme logout > should log the user out 1`] = ` +{ + "result": "You have logged out of ReadMe. Please use \`rdme login\` to login again.", + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme logout > should report the user as logged out if they aren't logged in 1`] = ` +{ + "result": "You have logged out of ReadMe. Please use \`rdme login\` to login again.", + "stderr": "", + "stdout": "", +} +`; diff --git a/__tests__/commands/__snapshots__/whoami.test.ts.snap b/__tests__/commands/__snapshots__/whoami.test.ts.snap new file mode 100644 index 000000000..143714935 --- /dev/null +++ b/__tests__/commands/__snapshots__/whoami.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`rdme whoami > should error if user is not authenticated 1`] = ` +{ + "error": [Error: Please login using \`rdme login\`.], + "stderr": "", + "stdout": "", +} +`; + +exports[`rdme whoami > should return the authenticated user 1`] = ` +{ + "result": "You are currently logged in as email@example.com to the subdomain project.", + "stderr": "", + "stdout": "", +} +`; diff --git a/__tests__/commands/login.test.ts b/__tests__/commands/login.test.ts index 67173a200..601f47899 100644 --- a/__tests__/commands/login.test.ts +++ b/__tests__/commands/login.test.ts @@ -2,10 +2,9 @@ import prompts from 'prompts'; import { describe, beforeAll, afterEach, it, expect } from 'vitest'; import Command from '../../src/commands/login.js'; -import { APIv1Error } from '../../src/lib/apiError.js'; import configStore from '../../src/lib/configstore.js'; import { getAPIv1Mock } from '../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../helpers/oclif.js'; +import { runCommand, type OclifOutput } from '../helpers/oclif.js'; const apiKey = 'abcdefg'; const email = 'user@example.com'; @@ -14,22 +13,22 @@ const project = 'subdomain'; const token = '123456'; describe('rdme login', () => { - let run: (args?: string[]) => Promise; + let run: (args?: string[]) => OclifOutput; beforeAll(() => { - run = runCommandAndReturnResult(Command); + run = runCommand(Command); }); afterEach(() => configStore.clear()); it('should error if no project provided', () => { prompts.inject([email, password]); - return expect(run()).rejects.toStrictEqual(new Error('No project subdomain provided. Please use `--project`.')); + return expect(run()).resolves.toMatchSnapshot(); }); it('should error if email is invalid', () => { prompts.inject(['this-is-not-an-email', password, project]); - return expect(run()).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + return expect(run()).resolves.toMatchSnapshot(); }); it('should post to /login on the API', async () => { @@ -37,7 +36,7 @@ describe('rdme login', () => { const mock = getAPIv1Mock().post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); - await expect(run()).resolves.toBe('Successfully logged in as user@example.com to the subdomain project.'); + await expect(run()).resolves.toMatchSnapshot(); mock.done(); @@ -51,9 +50,7 @@ describe('rdme login', () => { const mock = getAPIv1Mock().post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); - await expect(run(['--project', project])).resolves.toBe( - 'Successfully logged in as user@example.com to the subdomain project.', - ); + await expect(run(['--project', project])).resolves.toMatchSnapshot(); mock.done(); @@ -65,9 +62,9 @@ describe('rdme login', () => { it('should bypass prompts and post to /login on the API if passing in every opt', async () => { const mock = getAPIv1Mock().post('/api/v1/login', { email, password, project, token }).reply(200, { apiKey }); - await expect(run(['--email', email, '--password', password, '--project', project, '--otp', token])).resolves.toBe( - 'Successfully logged in as user@example.com to the subdomain project.', - ); + await expect( + run(['--email', email, '--password', password, '--project', project, '--otp', token]), + ).resolves.toMatchSnapshot(); mock.done(); @@ -79,9 +76,7 @@ describe('rdme login', () => { it('should bypass prompts and post to /login on the API if passing in every opt (no 2FA)', async () => { const mock = getAPIv1Mock().post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); - await expect(run(['--email', email, '--password', password, '--project', project])).resolves.toBe( - 'Successfully logged in as user@example.com to the subdomain project.', - ); + await expect(run(['--email', email, '--password', password, '--project', project])).resolves.toMatchSnapshot(); mock.done(); @@ -101,7 +96,7 @@ describe('rdme login', () => { const mock = getAPIv1Mock().post('/api/v1/login', { email, password, project }).reply(401, errorResponse); - await expect(run()).rejects.toStrictEqual(new APIv1Error(errorResponse)); + await expect(run()).resolves.toMatchSnapshot(); mock.done(); }); @@ -121,7 +116,7 @@ describe('rdme login', () => { .post('/api/v1/login', { email, password, project, token }) .reply(200, { apiKey }); - await expect(run()).resolves.toBe('Successfully logged in as user@example.com to the subdomain project.'); + await expect(run()).resolves.toMatchSnapshot(); mock.done(); @@ -144,7 +139,7 @@ describe('rdme login', () => { .post('/api/v1/login', { email, password, project: projectThatIsNotYours }) .reply(404, errorResponse); - await expect(run()).rejects.toStrictEqual(new APIv1Error(errorResponse)); + await expect(run()).resolves.toMatchSnapshot(); mock.done(); }); diff --git a/__tests__/commands/logout.test.ts b/__tests__/commands/logout.test.ts index 901e7dea9..577cb5bf5 100644 --- a/__tests__/commands/logout.test.ts +++ b/__tests__/commands/logout.test.ts @@ -1,15 +1,14 @@ import { describe, afterEach, beforeAll, it, expect } from 'vitest'; -import pkg from '../../package.json' with { type: 'json' }; import Command from '../../src/commands/logout.js'; import configStore from '../../src/lib/configstore.js'; -import { runCommandAndReturnResult } from '../helpers/oclif.js'; +import { runCommand, type OclifOutput } from '../helpers/oclif.js'; describe('rdme logout', () => { - let run: (args?: string[]) => Promise; + let run: (args?: string[]) => OclifOutput; beforeAll(() => { - run = runCommandAndReturnResult(Command); + run = runCommand(Command); }); afterEach(() => { @@ -20,18 +19,14 @@ describe('rdme logout', () => { configStore.delete('email'); configStore.delete('project'); - return expect(run()).resolves.toBe( - `You have logged out of ReadMe. Please use \`${pkg.name} login\` to login again.`, - ); + return expect(run()).resolves.toMatchSnapshot(); }); it('should log the user out', async () => { configStore.set('email', 'email@example.com'); configStore.set('project', 'subdomain'); - await expect(run()).resolves.toBe( - `You have logged out of ReadMe. Please use \`${pkg.name} login\` to login again.`, - ); + await expect(run()).resolves.toMatchSnapshot(); expect(configStore.get('email')).toBeUndefined(); expect(configStore.get('project')).toBeUndefined(); diff --git a/__tests__/commands/openapi/__snapshots__/convert.test.ts.snap b/__tests__/commands/openapi/__snapshots__/convert.test.ts.snap new file mode 100644 index 000000000..eee6c4f98 --- /dev/null +++ b/__tests__/commands/openapi/__snapshots__/convert.test.ts.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`rdme openapi convert > error handling > should warn if given an OpenAPI 3.0 definition (format: json) 1`] = ` +{ + "result": "Your API definition has been converted and bundled and saved to output.json!", + "stderr": "- Validating the API definition located at petstore.json... +⚠️ Warning! The input file is already OpenAPI, so no conversion is necessary. Any external references will be bundled. +", + "stdout": "", +} +`; + +exports[`rdme openapi convert > error handling > should warn if given an OpenAPI 3.0 definition (format: yaml) 1`] = ` +{ + "result": "Your API definition has been converted and bundled and saved to output.json!", + "stderr": "- Validating the API definition located at petstore.yaml... +⚠️ Warning! The input file is already OpenAPI, so no conversion is necessary. Any external references will be bundled. +", + "stdout": "", +} +`; + +exports[`rdme openapi convert > should convert with no prompts via opts 1`] = ` +{ + "result": "Your API definition has been converted and bundled and saved to output.json!", + "stderr": "- Validating the API definition located at petstore-simple.json... +", + "stdout": "", +} +`; diff --git a/__tests__/commands/openapi/__snapshots__/inspect.test.ts.snap b/__tests__/commands/openapi/__snapshots__/inspect.test.ts.snap index 4d888a9f9..ffd23ec8a 100644 --- a/__tests__/commands/openapi/__snapshots__/inspect.test.ts.snap +++ b/__tests__/commands/openapi/__snapshots__/inspect.test.ts.snap @@ -51,7 +51,7 @@ polymorphism: `; exports[`rdme openapi inspect > feature reports > should generate a report for '@readme/oas-examples/3.0/json/readme-…' (w/ [ 'readme' ]) 1`] = ` -" +[SoftError: x-default: You do not use this. x-readme.code-samples: · #/paths/~1x-code-samples/get/x-code-samples @@ -73,7 +73,7 @@ x-readme.samples-languages: · node · python · shell - · swift" + · swift] `; exports[`rdme openapi inspect > feature reports > should generate a report for '@readme/oas-examples/3.0/json/schema-…' (w/ [ 'additionalProperties', …(1) ]) 1`] = ` @@ -103,7 +103,7 @@ circularRefs: `; exports[`rdme openapi inspect > feature reports > should generate a report for '@readme/oas-examples/3.0/json/schema-…' (w/ [ 'additionalProperties', …(2) ]) 1`] = ` -" +[SoftError: additionalProperties: · #/components/schemas/BodyPart/properties/headers/additionalProperties · #/components/schemas/BodyPart/properties/mediaType/properties/parameters/additionalProperties @@ -132,11 +132,11 @@ x-readme.code-samples: You do not use this. x-readme.headers: You do not use this. x-readme.explorer-enabled: You do not use this. x-readme.proxy-enabled: You do not use this. -x-readme.samples-languages: You do not use this." +x-readme.samples-languages: You do not use this.] `; exports[`rdme openapi inspect > feature reports > should generate a report for '@readme/oas-examples/3.0/json/schema-…' (w/ [ 'circularRefs', 'readme' ]) 1`] = ` -" +[SoftError: circularRefs: · #/components/schemas/BodyPart/properties/parent · #/components/schemas/MultiPart/properties/bodyParts/items @@ -151,7 +151,7 @@ x-readme.code-samples: You do not use this. x-readme.headers: You do not use this. x-readme.explorer-enabled: You do not use this. x-readme.proxy-enabled: You do not use this. -x-readme.samples-languages: You do not use this." +x-readme.samples-languages: You do not use this.] `; exports[`rdme openapi inspect > feature reports > should generate a report for '@readme/oas-examples/3.1/json/train-t…' (w/ [ 'commonParameters' ]) 1`] = ` @@ -161,6 +161,15 @@ commonParameters: · #/paths/~1bookings~1{bookingId}~1payment/parameters" `; +exports[`rdme openapi inspect > feature reports > should throw an error if an invalid feature is supplied 1`] = ` +{ + "error": [Error: Expected --feature=reamde to be one of: additionalProperties, callbacks, circularRefs, commonParameters, discriminators, links, style, polymorphism, serverVariables, webhooks, xml, readme +See more help with --help], + "stderr": "", + "stdout": "", +} +`; + exports[`rdme openapi inspect > full reports > should generate a report for @readme/oas-examples/3.0/json/petstore.json 1`] = ` "Here are some interesting things we found in your API definition. 🕵️ diff --git a/__tests__/commands/openapi/__snapshots__/reduce.test.ts.snap b/__tests__/commands/openapi/__snapshots__/reduce.test.ts.snap new file mode 100644 index 000000000..75c85bc8a --- /dev/null +++ b/__tests__/commands/openapi/__snapshots__/reduce.test.ts.snap @@ -0,0 +1,67 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`rdme openapi reduce > error handling > should fail if given a Swagger 2.0 definition (format: json) 1`] = `[Error: Sorry, this reducer feature in rdme only supports OpenAPI 3.0+ definitions.]`; + +exports[`rdme openapi reduce > error handling > should fail if given a Swagger 2.0 definition (format: yaml) 1`] = `[Error: Sorry, this reducer feature in rdme only supports OpenAPI 3.0+ definitions.]`; + +exports[`rdme openapi reduce > error handling > should fail if you attempt to pass both tags and methods as opts 1`] = `[Error: You can pass in either tags or paths/methods, but not both.]`; + +exports[`rdme openapi reduce > error handling > should fail if you attempt to pass both tags and paths as opts 1`] = `[Error: You can pass in either tags or paths/methods, but not both.]`; + +exports[`rdme openapi reduce > error handling > should fail if you attempt to pass non-existent path and no method 1`] = `[Error: All paths in the API definition were removed. Did you supply the right path name to reduce by?]`; + +exports[`rdme openapi reduce > error handling > should fail if you attempt to reduce a spec to nothing via paths 1`] = `[Error: All paths in the API definition were removed. Did you supply the right path name to reduce by?]`; + +exports[`rdme openapi reduce > error handling > should fail if you attempt to reduce a spec to nothing via tags 1`] = `[Error: All paths in the API definition were removed. Did you supply the right path name to reduce by?]`; + +exports[`rdme openapi reduce > reducing > by path > should reduce and update title with no prompts via opts 1`] = ` +{ + "result": "Your reduced API definition has been saved to output.json! 🤏", + "stderr": "- Looking for API definitions... +- Validating the API definition located at petstore.json... +- Reducing your API definition... +✔ Reducing your API definition... done! ✅ +", + "stdout": "ℹ️ We found petstore.json and are attempting to reduce it. +", +} +`; + +exports[`rdme openapi reduce > reducing > by path > should reduce with no prompts via opts 1`] = ` +{ + "result": "Your reduced API definition has been saved to output.json! 🤏", + "stderr": "- Looking for API definitions... +- Validating the API definition located at petstore.json... +- Reducing your API definition... +✔ Reducing your API definition... done! ✅ +", + "stdout": "ℹ️ We found petstore.json and are attempting to reduce it. +", +} +`; + +exports[`rdme openapi reduce > reducing > by tag > should discover and upload an API definition if none is provided 1`] = ` +{ + "result": "Your reduced API definition has been saved to output.json! 🤏", + "stderr": "- Looking for API definitions... +- Validating the API definition located at petstore.json... +- Reducing your API definition... +✔ Reducing your API definition... done! ✅ +", + "stdout": "ℹ️ We found petstore.json and are attempting to reduce it. +", +} +`; + +exports[`rdme openapi reduce > reducing > by tag > should reduce with no prompts via opts 1`] = ` +{ + "result": "Your reduced API definition has been saved to output.json! 🤏", + "stderr": "- Looking for API definitions... +- Validating the API definition located at petstore.json... +- Reducing your API definition... +✔ Reducing your API definition... done! ✅ +", + "stdout": "ℹ️ We found petstore.json and are attempting to reduce it. +", +} +`; diff --git a/__tests__/commands/openapi/convert.test.ts b/__tests__/commands/openapi/convert.test.ts index 023f7cded..8c73637e6 100644 --- a/__tests__/commands/openapi/convert.test.ts +++ b/__tests__/commands/openapi/convert.test.ts @@ -6,18 +6,16 @@ import prompts from 'prompts'; import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, type MockInstance } from 'vitest'; import Command from '../../../src/commands/openapi/convert.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const successfulConversion = () => 'Your API definition has been converted and bundled and saved to output.json!'; +import { runCommand, type OclifOutput } from '../../helpers/oclif.js'; describe('rdme openapi convert', () => { let fsWriteFileSyncSpy: MockInstance; let reducedSpec: OASDocument; - let run: (args?: string[]) => Promise; + let run: (args?: string[]) => OclifOutput; let testWorkingDir: string; beforeAll(() => { - run = runCommandAndReturnResult(Command); + run = runCommand(Command); }); beforeEach(() => { @@ -42,7 +40,8 @@ describe('rdme openapi convert', () => { prompts.inject(['output.json']); - await expect(run([spec])).resolves.toBe(successfulConversion()); + const { result } = await run([spec]); + expect(result).toBe('Your API definition has been converted and bundled and saved to output.json!'); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(reducedSpec.tags).toHaveLength(1); @@ -62,7 +61,7 @@ describe('rdme openapi convert', () => { '--out', 'output.json', ]), - ).resolves.toBe(successfulConversion()); + ).resolves.toMatchSnapshot(); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/pet/{petId}']); @@ -70,14 +69,18 @@ describe('rdme openapi convert', () => { }); describe('error handling', () => { - it.each([['json'], ['yaml']])('should fail if given an OpenAPI 3.0 definition (format: %s)', async format => { - const spec = require.resolve(`@readme/oas-examples/3.0/${format}/petstore.${format}`); - - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it.each([['json'], ['yaml']])('should warn if given an OpenAPI 3.0 definition (format: %s)', async format => { + const spec = `petstore.${format}`; prompts.inject(['output.json']); - await expect(run([spec])).resolves.toBe(successfulConversion()); + await expect( + run([ + spec, + '--workingDirectory', + require.resolve(`@readme/oas-examples/3.0/${format}/${spec}`).replace(spec, ''), + ]), + ).resolves.toMatchSnapshot(); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(reducedSpec.tags).toHaveLength(3); @@ -98,12 +101,6 @@ describe('rdme openapi convert', () => { '/user/{username}', ]); expect(Object.keys(reducedSpec.paths['/pet/{petId}'])).toStrictEqual(['get', 'post', 'delete']); - - expect(consoleWarnSpy).toHaveBeenCalledWith( - '⚠️ Warning! The input file is already OpenAPI, so no conversion is necessary. Any external references will be bundled.', - ); - - consoleWarnSpy.mockRestore(); }); }); }); diff --git a/__tests__/commands/openapi/inspect.test.ts b/__tests__/commands/openapi/inspect.test.ts index 4ac04244d..e1152a923 100644 --- a/__tests__/commands/openapi/inspect.test.ts +++ b/__tests__/commands/openapi/inspect.test.ts @@ -1,16 +1,14 @@ /* eslint-disable @vitest/no-conditional-expect */ -import assert from 'node:assert'; - import { describe, it, expect, beforeAll } from 'vitest'; import Command from '../../../src/commands/openapi/inspect.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; +import { runCommand, type OclifOutput } from '../../helpers/oclif.js'; describe('rdme openapi inspect', () => { - let run: (args?: string[]) => Promise; + let run: (args?: string[]) => OclifOutput; beforeAll(() => { - run = runCommandAndReturnResult(Command); + run = runCommand(Command); }); describe('full reports', () => { @@ -19,18 +17,16 @@ describe('rdme openapi inspect', () => { '@readme/oas-examples/3.0/json/readme.json', '@readme/oas-examples/3.0/json/readme-extensions.json', '@readme/oas-examples/3.1/json/train-travel.json', - ])('should generate a report for %s', spec => { - return expect(run([require.resolve(spec)])).resolves.toMatchSnapshot(); + ])('should generate a report for %s', async spec => { + const { result } = await run([require.resolve(spec)]); + expect(result).toMatchSnapshot(); }); }); describe('feature reports', () => { it('should throw an error if an invalid feature is supplied', () => { const spec = require.resolve('@readme/oas-examples/3.0/json/readme-extensions.json'); - - return expect(run([spec, '--feature', 'style', '--feature', 'reamde'])).rejects.toThrow( - 'Expected --feature=reamde to be one of:', - ); + return expect(run([spec, '--feature', 'style', '--feature', 'reamde'])).resolves.toMatchSnapshot(); }); const cases: { feature: string[]; shouldSoftError?: true; spec: string }[] = [ @@ -67,19 +63,11 @@ describe('rdme openapi inspect', () => { it.each(cases)('should generate a report for $spec (w/ $feature)', async ({ spec, feature, shouldSoftError }) => { const args = [require.resolve(spec)].concat(...feature.map(f => ['--feature', f])); + const { result, error } = await run(args); if (!shouldSoftError) { - await expect(run(args)).resolves.toMatchSnapshot(); - - return; - } - - try { - await run(args); - - assert.fail('A soft error should have been thrown for this test case.'); - } catch (err) { - expect(err.name).toBe('SoftError'); - expect(err.message).toMatchSnapshot(); + expect(result).toMatchSnapshot(); + } else { + expect(error).toMatchSnapshot(); } }); }); diff --git a/__tests__/commands/openapi/reduce.test.ts b/__tests__/commands/openapi/reduce.test.ts index 21e75bc3f..e6d259404 100644 --- a/__tests__/commands/openapi/reduce.test.ts +++ b/__tests__/commands/openapi/reduce.test.ts @@ -1,32 +1,24 @@ -/* eslint-disable no-console */ import type { OASDocument } from 'oas/types'; import fs from 'node:fs'; -import chalk from 'chalk'; import prompts from 'prompts'; import { describe, beforeAll, beforeEach, afterEach, it, expect, vi, type MockInstance } from 'vitest'; import Command from '../../../src/commands/openapi/reduce.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const successfulReduction = () => 'Your reduced API definition has been saved to output.json! 🤏'; - -let consoleInfoSpy: MockInstance; -const getCommandOutput = () => consoleInfoSpy.mock.calls.join('\n\n'); +import { runCommand, type OclifOutput } from '../../helpers/oclif.js'; describe('rdme openapi reduce', () => { let fsWriteFileSyncSpy: MockInstance; let reducedSpec: OASDocument; - let run: (args?: string[]) => Promise; + let run: (args?: string[]) => OclifOutput; let testWorkingDir: string; beforeAll(() => { - run = runCommandAndReturnResult(Command); + run = runCommand(Command); }); beforeEach(() => { - consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); testWorkingDir = process.cwd(); fsWriteFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementationOnce((filename, data) => { reducedSpec = JSON.parse(data as string); @@ -34,7 +26,6 @@ describe('rdme openapi reduce', () => { }); afterEach(() => { - consoleInfoSpy.mockRestore(); process.chdir(testWorkingDir); vi.restoreAllMocks(); }); @@ -51,7 +42,8 @@ describe('rdme openapi reduce', () => { prompts.inject(['tags', ['pet'], 'output.json']); - await expect(run([spec])).resolves.toBe(successfulReduction()); + const { result } = await run([spec]); + expect(result).toBe('Your reduced API definition has been saved to output.json! 🤏'); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(reducedSpec.tags).toHaveLength(1); @@ -65,26 +57,16 @@ describe('rdme openapi reduce', () => { }); it('should discover and upload an API definition if none is provided', async () => { - const spec = 'petstore.json'; - prompts.inject(['tags', ['user'], 'output.json']); - await expect(run(['--workingDirectory', './__tests__/__fixtures__/relative-ref-oas'])).resolves.toBe( - successfulReduction(), - ); - - expect(console.info).toHaveBeenCalledTimes(1); - - const output = getCommandOutput(); - - expect(output).toBe(chalk.yellow(`ℹ️ We found ${spec} and are attempting to reduce it.`)); + await expect( + run(['--workingDirectory', './__tests__/__fixtures__/relative-ref-oas']), + ).resolves.toMatchSnapshot(); expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/user']); }); it('should reduce with no prompts via opts', async () => { - const spec = 'petstore.json'; - await expect( run([ '--workingDirectory', @@ -94,13 +76,7 @@ describe('rdme openapi reduce', () => { '--out', 'output.json', ]), - ).resolves.toBe(successfulReduction()); - - expect(console.info).toHaveBeenCalledTimes(1); - - const output = getCommandOutput(); - - expect(output).toBe(chalk.yellow(`ℹ️ We found ${spec} and are attempting to reduce it.`)); + ).resolves.toMatchSnapshot(); expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/user']); }); @@ -117,7 +93,8 @@ describe('rdme openapi reduce', () => { prompts.inject(['paths', ['/pet', '/pet/findByStatus'], ['get', 'post'], 'output.json']); - await expect(run([spec])).resolves.toBe(successfulReduction()); + const { result } = await run([spec]); + expect(result).toBe('Your reduced API definition has been saved to output.json! 🤏'); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(reducedSpec.tags).toHaveLength(1); @@ -127,8 +104,6 @@ describe('rdme openapi reduce', () => { }); it('should reduce with no prompts via opts', async () => { - const spec = 'petstore.json'; - await expect( run([ '--workingDirectory', @@ -144,13 +119,7 @@ describe('rdme openapi reduce', () => { '--out', 'output.json', ]), - ).resolves.toBe(successfulReduction()); - - expect(console.info).toHaveBeenCalledTimes(1); - - const output = getCommandOutput(); - - expect(output).toBe(chalk.yellow(`ℹ️ We found ${spec} and are attempting to reduce it.`)); + ).resolves.toMatchSnapshot(); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/pet', '/pet/{petId}']); @@ -159,7 +128,6 @@ describe('rdme openapi reduce', () => { }); it('should reduce and update title with no prompts via opts', async () => { - const spec = 'petstore.json'; const title = 'some alternative title'; await expect( @@ -179,13 +147,7 @@ describe('rdme openapi reduce', () => { '--out', 'output.json', ]), - ).resolves.toBe(successfulReduction()); - - expect(console.info).toHaveBeenCalledTimes(1); - - const output = getCommandOutput(); - - expect(output).toBe(chalk.yellow(`ℹ️ We found ${spec} and are attempting to reduce it.`)); + ).resolves.toMatchSnapshot(); expect(fsWriteFileSyncSpy).toHaveBeenCalledWith('output.json', expect.any(String)); expect(Object.keys(reducedSpec.paths)).toStrictEqual(['/pet', '/pet/{petId}']); @@ -200,9 +162,8 @@ describe('rdme openapi reduce', () => { it.each([['json'], ['yaml']])('should fail if given a Swagger 2.0 definition (format: %s)', async format => { const spec = require.resolve(`@readme/oas-examples/2.0/${format}/petstore.${format}`); - await expect(run([spec])).rejects.toStrictEqual( - new Error('Sorry, this reducer feature in rdme only supports OpenAPI 3.0+ definitions.'), - ); + const { error } = await run([spec]); + expect(error).toMatchSnapshot(); }); it('should fail if you attempt to reduce a spec to nothing via tags', async () => { @@ -210,9 +171,8 @@ describe('rdme openapi reduce', () => { prompts.inject(['tags', ['unknown-tag'], 'output.json']); - await expect(run([spec])).rejects.toStrictEqual( - new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?'), - ); + const { error } = await run([spec]); + expect(error).toMatchSnapshot(); }); it('should fail if you attempt to reduce a spec to nothing via paths', async () => { @@ -220,33 +180,29 @@ describe('rdme openapi reduce', () => { prompts.inject(['paths', ['unknown-path'], 'output.json']); - await expect(run([spec])).rejects.toStrictEqual( - new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?'), - ); + const { error } = await run([spec]); + expect(error).toMatchSnapshot(); }); it('should fail if you attempt to pass both tags and paths as opts', async () => { const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); - await expect(run([spec, '--tag', 'tag1', '--tag', 'tag2', '--path', '/path'])).rejects.toStrictEqual( - new Error('You can pass in either tags or paths/methods, but not both.'), - ); + const { error } = await run([spec, '--tag', 'tag1', '--tag', 'tag2', '--path', '/path']); + expect(error).toMatchSnapshot(); }); it('should fail if you attempt to pass both tags and methods as opts', async () => { const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); - await expect(run([spec, '--tag', 'tag1', '--tag', 'tag2', '--method', 'get'])).rejects.toStrictEqual( - new Error('You can pass in either tags or paths/methods, but not both.'), - ); + const { error } = await run([spec, '--tag', 'tag1', '--tag', 'tag2', '--method', 'get']); + expect(error).toMatchSnapshot(); }); it('should fail if you attempt to pass non-existent path and no method', async () => { const spec = require.resolve('@readme/oas-examples/3.0/json/petstore.json'); - await expect(run([spec, '--path', 'unknown-path'])).rejects.toStrictEqual( - new Error('All paths in the API definition were removed. Did you supply the right path name to reduce by?'), - ); + const { error } = await run([spec, '--path', '/unknown-path']); + expect(error).toMatchSnapshot(); }); }); }); diff --git a/__tests__/commands/openapi/validate.test.ts b/__tests__/commands/openapi/validate.test.ts index 995e97cb8..c732ce458 100644 --- a/__tests__/commands/openapi/validate.test.ts +++ b/__tests__/commands/openapi/validate.test.ts @@ -31,9 +31,10 @@ describe('rdme openapi validate', () => { ['OpenAPI 3.1', 'json', '3.1'], ['OpenAPI 3.1', 'yaml', '3.1'], ])('should support validating a %s definition (format: %s)', async (_, format, specVersion) => { - expect( - (await run([require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`)])).result, - ).toContain(`petstore.${format} is a valid ${specVersion === '2.0' ? 'Swagger' : 'OpenAPI'} API definition!`); + const { result } = await run([require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`)]); + expect(result).toContain( + `petstore.${format} is a valid ${specVersion === '2.0' ? 'Swagger' : 'OpenAPI'} API definition!`, + ); }); it('should discover and upload an API definition if none is provided', async () => { diff --git a/__tests__/commands/whoami.test.ts b/__tests__/commands/whoami.test.ts index aecd0bbee..f7fcdfb47 100644 --- a/__tests__/commands/whoami.test.ts +++ b/__tests__/commands/whoami.test.ts @@ -1,15 +1,14 @@ import { describe, afterEach, it, expect, beforeAll } from 'vitest'; -import pkg from '../../package.json' with { type: 'json' }; import Command from '../../src/commands/whoami.js'; import configStore from '../../src/lib/configstore.js'; -import { runCommandAndReturnResult } from '../helpers/oclif.js'; +import { runCommand, type OclifOutput } from '../helpers/oclif.js'; describe('rdme whoami', () => { - let run: (args?: string[]) => Promise; + let run: (args?: string[]) => OclifOutput; beforeAll(() => { - run = runCommandAndReturnResult(Command); + run = runCommand(Command); }); afterEach(() => { @@ -20,13 +19,13 @@ describe('rdme whoami', () => { configStore.delete('email'); configStore.delete('project'); - return expect(run()).rejects.toStrictEqual(new Error(`Please login using \`${pkg.name} login\`.`)); + return expect(run()).resolves.toMatchSnapshot(); }); it('should return the authenticated user', () => { configStore.set('email', 'email@example.com'); configStore.set('project', 'subdomain'); - return expect(run()).resolves.toBe('You are currently logged in as email@example.com to the subdomain project.'); + return expect(run()).resolves.toMatchSnapshot(); }); }); diff --git a/__tests__/helpers/oclif.ts b/__tests__/helpers/oclif.ts index d422010ba..a8db897a9 100644 --- a/__tests__/helpers/oclif.ts +++ b/__tests__/helpers/oclif.ts @@ -50,6 +50,8 @@ export function runCommand(Command: CommandClass) { * an error if the command throws one. Mainly a helper to minimize the amount of refactoring * in our existing tests. * + * @deprecated This is a legacy helper to aid with the initial migration to `oclif`. + * Use `runCommand` for all new tests instead. * @example runCommandAndReturnResult(LoginCommand)(['--email', 'owlbert@example.com', '--password', 'password']) */ export function runCommandAndReturnResult(Command: CommandClass) {