diff --git a/__tests__/commands/openapi/index.test.ts b/__tests__/commands/openapi/index.test.ts deleted file mode 100644 index 95b23512d..000000000 --- a/__tests__/commands/openapi/index.test.ts +++ /dev/null @@ -1,1440 +0,0 @@ -/* eslint-disable no-console */ - -import fs from 'node:fs'; - -import chalk from 'chalk'; -import nock from 'nock'; -import prompts from 'prompts'; -import { describe, beforeAll, beforeEach, afterEach, it, expect, vi, type MockInstance } from 'vitest'; - -import Command from '../../../src/commands/openapi/index.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import petstoreWeird from '../../__fixtures__/petstore-simple-weird-version.json' with { type: 'json' }; -import { getAPIV1Mock, getAPIV1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import { after, before } from '../../helpers/get-gha-setup.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; -import { after as afterGHAEnv, before as beforeGHAEnv } from '../../helpers/setup-gha-env.js'; - -let consoleInfoSpy: MockInstance; -let consoleWarnSpy: MockInstance; - -const key = 'API_KEY'; -const id = '5aa0409b7cf527a93bfb44df'; -const version = '1.0.0'; -const exampleRefLocation = 'https://dash.example.com/project/example-project/1.0.1/refs/ex'; -const successfulMessageBase = (specPath, specType) => [ - '', - `\t${chalk.green(exampleRefLocation)}`, - '', - `To update your ${specType} definition, run the following:`, - '', - `\t${chalk.green(`rdme openapi ${specPath} --key= --id=1`)}`, -]; -const successfulUpload = (specPath, specType = 'OpenAPI') => - [ - `You've successfully uploaded a new ${specType} file to your ReadMe project!`, - ...successfulMessageBase(specPath, specType), - ].join('\n'); - -const successfulUpdate = (specPath, specType = 'OpenAPI') => - [ - `You've successfully updated an existing ${specType} file on your ReadMe project!`, - ...successfulMessageBase(specPath, specType), - ].join('\n'); - -const getCommandOutput = () => { - return [consoleWarnSpy.mock.calls.join('\n\n'), consoleInfoSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); -}; - -const getRandomRegistryId = () => Math.random().toString(36).substring(2); - -describe('rdme openapi', () => { - let run: (args?: string[]) => Promise; - let testWorkingDir: string; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - beforeEach(() => { - consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - testWorkingDir = process.cwd(); - }); - - afterEach(() => { - consoleInfoSpy.mockRestore(); - consoleWarnSpy.mockRestore(); - - process.chdir(testWorkingDir); - - nock.cleanAll(); - }); - - describe('upload', () => { - it.each([ - ['Swagger 2.0', 'json', '2.0', 'Swagger'], - ['Swagger 2.0', 'yaml', '2.0', 'Swagger'], - ['OpenAPI 3.0', 'json', '3.0', 'OpenAPI'], - ['OpenAPI 3.0', 'yaml', '3.0', 'OpenAPI'], - ['OpenAPI 3.1', 'json', '3.1', 'OpenAPI'], - ['OpenAPI 3.1', 'yaml', '3.1', 'OpenAPI'], - - // Postman collections get automatically converted to OpenAPI 3.0 by `oas-normalize`. - ['Postman', 'json', '3.0', 'Postman'], - ['Postman', 'yaml', '3.0', 'Postman'], - ])('should support uploading a %s definition (format: %s)', async (_, format, specVersion, type) => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: specVersion } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - let spec; - if (type === 'Postman') { - spec = require.resolve(`../../__fixtures__/postman/petstore.collection.${format}`); - } else { - spec = require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`); - } - - await expect(run(['--key', key, '--version', version, spec])).resolves.toBe(successfulUpload(spec, type)); - - expect(console.info).toHaveBeenCalledTimes(0); - - postMock.done(); - return mock.done(); - }); - - it('should create a new spec via prompts', async () => { - prompts.inject(['create']); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [{ _id: 'spec1', title: 'spec1_title' }]) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, '--version', version, spec])).resolves.toBe(successfulUpload(spec)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should create a new spec via `--create` flag', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, '--version', version, spec, '--create'])).resolves.toBe(successfulUpload(spec)); - - postMock.done(); - return mock.done(); - }); - - it('should create a new spec via `--create` flag and ignore `--id`', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }]) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, '--id', 'some-id', spec, '--create'])).resolves.toBe(successfulUpload(spec)); - - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.info).toHaveBeenCalledTimes(0); - - const output = getCommandOutput(); - - expect(output).toMatch(/the `--id` parameter will be ignored/i); - - postMock.done(); - return mock.done(); - }); - - it('should bundle and upload the expected content', async () => { - let requestBody; - const registryUUID = getRandomRegistryId(); - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, '--version', version, spec])).resolves.toBe(successfulUpload(spec)); - - expect(console.info).toHaveBeenCalledTimes(0); - - expect(requestBody).toMatchSnapshot(); - - postMock.done(); - return mock.done(); - }); - - it('should update title, bundle and upload the expected content', async () => { - let requestBody; - const registryUUID = getRandomRegistryId(); - const title = 'some alternative title'; - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, '--version', version, spec, '--title', title])).resolves.toBe( - successfulUpload(spec), - ); - - expect(console.info).toHaveBeenCalledTimes(0); - - expect(requestBody).toMatchSnapshot(); - - postMock.done(); - return mock.done(); - }); - - it('should upload the expected content and return raw output', async () => { - let requestBody; - const registryUUID = getRandomRegistryId(); - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, '--version', version, spec, '--raw'])).resolves.toMatchSnapshot(); - - postMock.done(); - return mock.done(); - }); - }); - - describe('updates / resyncs', () => { - it.each([ - ['Swagger 2.0', 'json', '2.0', 'Swagger'], - ['Swagger 2.0', 'yaml', '2.0', 'Swagger'], - ['OpenAPI 3.0', 'json', '3.0', 'OpenAPI'], - ['OpenAPI 3.0', 'yaml', '3.0', 'OpenAPI'], - ['OpenAPI 3.1', 'json', '3.1', 'OpenAPI'], - ['OpenAPI 3.1', 'yaml', '3.1', 'OpenAPI'], - ])('should support updating a %s definition (format: %s)', async (_, format, specVersion, type) => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: specVersion } }); - - const putMock = getAPIV1MockWithVersionHeader(version) - .put(`/api/v1/api-specification/${id}`, { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = require.resolve(`@readme/oas-examples/${specVersion}/${format}/petstore.${format}`); - - await expect(run(['--key', key, '--id', id, spec, '--version', version])).resolves.toBe( - successfulUpdate(spec, type), - ); - - putMock.done(); - return mock.done(); - }); - - it('should return warning if providing `id` and `version`', async () => { - expect.assertions(4); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const putMock = getAPIV1MockWithVersionHeader(version) - .put(`/api/v1/api-specification/${id}`, { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = require.resolve('@readme/oas-examples/3.1/json/petstore.json'); - - await expect(run(['--key', key, '--id', id, spec, '--version', version])).resolves.toBe(successfulUpdate(spec)); - - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.info).toHaveBeenCalledTimes(0); - - const output = getCommandOutput(); - - expect(output).toMatch(/the `--version` option will be ignored/i); - - putMock.done(); - return mock.done(); - }); - - it('should update a spec via prompts', async () => { - prompts.inject(['update', 'spec2']); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [ - { _id: 'spec1', title: 'spec1_title' }, - { _id: 'spec2', title: 'spec2_title' }, - ]) - .put('/api/v1/api-specification/spec2', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, spec, '--version', version])).resolves.toBe(successfulUpdate(spec)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should discover and upload an API definition if none is provided', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => { - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = 'petstore.json'; - - await expect( - run(['--key', key, '--version', version, '--workingDirectory', './__tests__/__fixtures__/relative-ref-oas']), - ).resolves.toBe(successfulUpload(spec)); - - expect(console.info).toHaveBeenCalledTimes(1); - - const output = getCommandOutput(); - expect(output).toBe(chalk.yellow(`ℹī¸ We found ${spec} and are attempting to upload it.`)); - - postMock.done(); - return mock.done(); - }); - - it('should use specified working directory and upload the expected content', async () => { - let requestBody; - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = 'petstore.json'; - - await expect( - run([ - spec, - '--key', - key, - '--version', - version, - '--workingDirectory', - './__tests__/__fixtures__/relative-ref-oas', - ]), - ).resolves.toBe(successfulUpload(spec)); - - expect(console.info).toHaveBeenCalledTimes(0); - - expect(requestBody).toMatchSnapshot(); - - postMock.done(); - return mock.done(); - }); - - it('should return spec update info for dry run', async () => { - prompts.inject(['update', 'spec2']); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [ - { _id: 'spec1', title: 'spec1_title' }, - { _id: 'spec2', title: 'spec2_title' }, - ]); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, spec, '--version', version, '--dryRun'])).resolves.toMatch( - `dry run! The API Definition located at ${spec} will update this API Definition ID: spec2`, - ); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should return spec create info for dry run (with working directory)', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => { - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - await expect( - run([ - '--key', - key, - '--version', - version, - '--workingDirectory', - './__tests__/__fixtures__/relative-ref-oas', - '--dryRun', - ]), - ).resolves.toMatch( - '🎭 dry run! The API Definition located at petstore.json will be created for this project version: 1.0.0', - ); - - const output = getCommandOutput(); - expect(output).toMatch( - chalk.yellow('🎭 dry run option detected! No API definitions will be created or updated in ReadMe.'), - ); - - return mock.done(); - }); - - describe('--update', () => { - it("should update a spec file without prompts if providing `update` and it's the one spec available", async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [{ _id: 'spec1', title: 'spec1_title' }]) - .put('/api/v1/api-specification/spec1', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, spec, '--version', version, '--update'])).resolves.toBe(successfulUpdate(spec)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should error if providing `update` and there are multiple specs available', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [ - { _id: 'spec1', title: 'spec1_title' }, - { _id: 'spec2', title: 'spec2_title' }, - ]); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, spec, '--version', version, '--update'])).rejects.toStrictEqual( - new Error( - "The `--update` option cannot be used when there's more than one API definition available (found 2).", - ), - ); - return mock.done(); - }); - - it('should warn if providing both `update` and `id`', async () => { - expect.assertions(5); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .put('/api/v1/api-specification/spec1', { registryUUID }) - .basicAuth({ user: key }) - .reply(function (uri, rBody, cb) { - expect(this.req.headers['x-readme-version']).toBeUndefined(); - return cb(null, [201, { _id: 1 }, { location: exampleRefLocation }]); - }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, spec, '--id', 'spec1', '--update'])).resolves.toBe(successfulUpdate(spec)); - - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.info).toHaveBeenCalledTimes(0); - - const output = getCommandOutput(); - expect(output).toMatch(/the `--update` parameter will be ignored./); - return mock.done(); - }); - }); - - it.todo('should paginate to next and previous pages of specs'); - }); - - describe('versioning', () => { - it('should use version from version param properly', async () => { - expect.assertions(2); - let requestBody = ''; - const registryUUID = getRandomRegistryId(); - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(function (uri, rBody, cb) { - expect(this.req.headers['x-readme-version']).toBe(version); - return cb(null, [201, { _id: 1 }, { location: exampleRefLocation }]); - }); - - const spec = './__tests__/__fixtures__/petstore-simple-weird-version.json'; - - await expect(run(['--key', key, '--version', version, spec])).resolves.toBe(successfulUpload(spec)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should use version from spec file properly', async () => { - expect.assertions(2); - const specVersion = '1.2.3'; - let requestBody = ''; - const registryUUID = getRandomRegistryId(); - const mock = getAPIV1Mock() - .get(`/api/v1/version/${specVersion}`) - .basicAuth({ user: key }) - .reply(200, { version: specVersion }) - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(specVersion) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(function (uri, rBody, cb) { - expect(this.req.headers['x-readme-version']).toBe(specVersion); - return cb(null, [201, { _id: 1 }, { location: exampleRefLocation }]); - }); - - const spec = './__tests__/__fixtures__/petstore-simple-weird-version.json'; - - await expect(run(['--key', key, spec, '--version', version, '--useSpecVersion'])).resolves.toBe( - successfulUpload(spec), - ); - - mockWithHeader.done(); - return mock.done(); - }); - - describe('CI version handling', () => { - beforeEach(() => { - process.env.TEST_RDME_CI = 'true'; - }); - - afterEach(() => { - delete process.env.TEST_RDME_CI; - }); - - it('should omit version header in CI environment', async () => { - expect.assertions(2); - let requestBody = ''; - const registryUUID = getRandomRegistryId(); - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => { - requestBody = body.substring(body.indexOf('{'), body.lastIndexOf('}') + 1); - requestBody = JSON.parse(requestBody); - - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(function (uri, rBody, cb) { - expect(this.req.headers['x-readme-version']).toBeUndefined(); - return cb(null, [201, { _id: 1 }, { location: exampleRefLocation }]); - }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run(['--key', key, spec])).resolves.toBe(successfulUpload(spec)); - - return mock.done(); - }); - }); - - it('should error if version flag sent to API returns a 404', async () => { - const invalidVersion = 'v1000'; - - const errorObject = { - error: 'VERSION_NOTFOUND', - message: `The version you specified (${invalidVersion}) doesn't match any of the existing versions (1.0) in ReadMe.`, - suggestion: - 'You can pass the version in via the `x-readme-version` header. If you want to create a new version, do so in the Versions section inside ReadMe. Note that the version in the URL is our API version, not the version of your docs.', - docs: 'https://docs.readme.com/logs/xx-xx-xx', - help: "If you need help, email support@readme.io and include the following link to your API log: 'https://docs.readme.com/logs/xx-xx-xx'.", - poem: [ - 'We looked high and low,', - 'Searched up, down and around.', - "You'll have to give it another go,", - `Because version ${invalidVersion}'s not found!`, - ], - }; - - const mock = getAPIV1Mock().get(`/api/v1/version/${invalidVersion}`).reply(404, errorObject); - - await expect( - run([ - '--key', - key, - require.resolve('@readme/oas-examples/3.1/json/petstore.json'), - '--version', - invalidVersion, - ]), - ).rejects.toStrictEqual(new APIv1Error(errorObject)); - - return mock.done(); - }); - - it('should request a version list if version is not found', async () => { - const selectedVersion = '1.0.1'; - prompts.inject([selectedVersion]); - - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version: '1.0.0' }, { version: '1.0.1' }]) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(selectedVersion) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = require.resolve('@readme/oas-examples/2.0/json/petstore.json'); - - await expect(run(['--key', key, spec])).resolves.toBe(successfulUpload(spec, 'Swagger')); - - mockWithHeader.done(); - return mock.done(); - }); - }); - - describe('error handling', () => { - it('should error if `--create` and `--update` flags are passed simultaneously', () => { - return expect(run(['--key', key, '--create', '--update'])).rejects.toThrow( - '--update=true cannot also be provided when using --create', - ); - }); - - it('should error if invalid API key is sent and version list does not load', async () => { - const errorObject = { - error: 'APIKEY_NOTFOUND', - message: "We couldn't find your API key.", - suggestion: - "The API key you passed in (API_KEY) doesn't match any keys we have in our system. API keys must be passed in as the username part of basic auth. You can get your API key in Configuration > API Key, or in the docs.", - docs: 'https://docs.readme.com/logs/xx-xx-xx', - help: "If you need help, email support@readme.io and include the following link to your API log: 'https://docs.readme.com/logs/xx-xx-xx'.", - poem: [ - 'The ancient gatekeeper declares:', - "'To pass, reveal your API key.'", - "'API_KEY', you start to ramble", - 'Oops, you remembered it poorly!', - ], - }; - - const mock = getAPIV1Mock().get('/api/v1/version').reply(401, errorObject); - - await expect( - run([require.resolve('@readme/oas-examples/3.1/json/petstore.json'), '--key', 'key']), - ).rejects.toStrictEqual(new APIv1Error(errorObject)); - - return mock.done(); - }); - - it('should throw an error if an invalid OpenAPI 3.0 definition is supplied', () => { - return expect( - run(['./__tests__/__fixtures__/invalid-oas.json', '--key', key, '--id', id, '--version', version]), - ).rejects.toMatchSnapshot(); - }); - - it('should throw an error if an invalid OpenAPI 3.1 definition is supplied', () => { - return expect( - run(['./__tests__/__fixtures__/invalid-oas-3.1.json', '--key', key, '--id', id, '--version', version]), - ).rejects.toMatchSnapshot(); - }); - - it('should throw an error if an invalid ref is supplied', () => { - return expect( - run(['./__tests__/__fixtures__/invalid-ref-oas/petstore.json', '--key', key, '--id', id, '--version', version]), - ).rejects.toMatchSnapshot(); - }); - - it('should throw an error if an invalid Swagger definition is supplied (create)', async () => { - const errorObject = { - error: 'INTERNAL_ERROR', - message: 'Unknown error (README VALIDATION ERROR "x-samples-languages" must be of type "Array")', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(400, errorObject); - - await expect( - run(['./__tests__/__fixtures__/swagger-with-invalid-extensions.json', '--key', key, '--version', version]), - ).rejects.toStrictEqual(new APIv1Error(errorObject)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should throw an error if an invalid Swagger definition is supplied (update)', async () => { - const errorObject = { - error: 'INTERNAL_ERROR', - message: 'Unknown error (README VALIDATION ERROR "x-samples-languages" must be of type "Array")', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const putMock = getAPIV1MockWithVersionHeader(version) - .put(`/api/v1/api-specification/${id}`, { registryUUID }) - .basicAuth({ user: key }) - .reply(400, errorObject); - - await expect( - run([ - './__tests__/__fixtures__/swagger-with-invalid-extensions.json', - '--key', - key, - '--id', - id, - '--version', - version, - ]), - ).rejects.toStrictEqual(new APIv1Error(errorObject)); - - putMock.done(); - return mock.done(); - }); - - it('should throw an error if registry upload fails', async () => { - const errorObject = { - error: 'INTERNAL_ERROR', - message: 'Unknown error (Registry is offline? lol idk)', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(400, errorObject); - - await expect( - run(['./__tests__/__fixtures__/swagger-with-invalid-extensions.json', '--key', key, '--version', version]), - ).rejects.toStrictEqual(new APIv1Error(errorObject)); - - return mock.done(); - }); - - it('should error if API errors', async () => { - const errorObject = { - error: 'SPEC_VERSION_NOTFOUND', - message: - "The version you specified ({version}) doesn't match any of the existing versions ({versions_list}) in ReadMe.", - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(400, errorObject); - - await expect( - run([require.resolve('@readme/oas-examples/2.0/json/petstore.json'), '--key', key, '--version', version]), - ).rejects.toStrictEqual(new APIv1Error(errorObject)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should error if API errors (generic upload error)', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(400, 'some non-JSON upload error'); - - await expect( - run([require.resolve('@readme/oas-examples/2.0/json/petstore.json'), '--key', key, '--version', version]), - ).rejects.toStrictEqual( - new Error( - 'Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at support@readme.io.', - ), - ); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should error if API errors (request timeout)', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version: '1.0.0' }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(500, 'Application Error'); - - await expect( - run([require.resolve('@readme/oas-examples/2.0/json/petstore.json'), '--key', key, '--version', version]), - ).rejects.toStrictEqual( - new Error( - "We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks.", - ), - ); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should error if no file was provided or able to be discovered', () => { - return expect(run(['--key', key, '--version', version, '--workingDirectory', 'bin'])).rejects.toStrictEqual( - new Error( - "We couldn't find an OpenAPI or Swagger definition.\n\nPlease specify the path to your definition with `rdme openapi ./path/to/api/definition`.", - ), - ); - }); - }); - - describe('GHA onboarding E2E tests', () => { - let yamlOutput; - - beforeEach(() => { - before((fileName, data) => { - yamlOutput = data; - }); - }); - - afterEach(() => { - after(); - }); - - it('should create GHA workflow (create spec)', async () => { - expect.assertions(6); - const yamlFileName = 'openapi-file'; - prompts.inject(['create', true, 'openapi-branch', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [{ _id: 'spec1', title: 'spec1_title' }]) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', version])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${yamlFileName}.yml`, expect.any(String)); - expect(console.info).toHaveBeenCalledTimes(2); - const output = getCommandOutput(); - expect(output).toMatch("Looks like you're running this command in a GitHub Repository!"); - expect(output).toMatch('successfully uploaded a new OpenAPI file to your ReadMe project'); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should create GHA workflow (--github flag enabled)', async () => { - expect.assertions(6); - const yamlFileName = 'openapi-file-github-flag'; - prompts.inject(['create', 'openapi-branch-github-flag', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [{ _id: 'spec1', title: 'spec1_title' }]) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', version, '--github'])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${yamlFileName}.yml`, expect.any(String)); - expect(console.info).toHaveBeenCalledTimes(2); - const output = getCommandOutput(); - expect(output).toMatch("Let's get you set up with GitHub Actions!"); - expect(output).toMatch('successfully uploaded a new OpenAPI file to your ReadMe project'); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should create GHA workflow (update spec via prompt)', async () => { - expect.assertions(3); - const yamlFileName = 'openapi-file-update-prompt'; - prompts.inject(['update', 'spec2', true, 'openapi-branch-update-prompt', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [ - { _id: 'spec1', title: 'spec1_title' }, - { _id: 'spec2', title: 'spec2_title' }, - ]) - .put('/api/v1/api-specification/spec2', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 'spec2' }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', version])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${yamlFileName}.yml`, expect.any(String)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should create GHA workflow (--create flag enabled)', async () => { - expect.assertions(3); - const yamlFileName = 'openapi-file-create-flag'; - const altVersion = '1.0.1'; - prompts.inject([true, 'openapi-branch-create-flag', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${altVersion}`) - .basicAuth({ user: key }) - .reply(200, { version: altVersion }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(altVersion) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', altVersion, '--create'])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${yamlFileName}.yml`, expect.any(String)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should create GHA workflow (--create flag enabled with ignored id opt)', async () => { - expect.assertions(3); - const yamlFileName = 'openapi-file-create-flag-id-opt'; - prompts.inject([version, true, 'openapi-branch-create-flag-id-opt', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }, { version: '1.1.0' }]) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--id', 'some-id', '--create'])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${yamlFileName}.yml`, expect.any(String)); - - postMock.done(); - return mock.done(); - }); - - it('should create GHA workflow (--update flag enabled)', async () => { - expect.assertions(3); - const yamlFileName = 'openapi-file-update-flag'; - prompts.inject([true, 'openapi-branch-update-flag', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [{ _id: 'spec1', title: 'spec1_title' }]) - .put('/api/v1/api-specification/spec1', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', version, '--update'])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${yamlFileName}.yml`, expect.any(String)); - - mockWithHeader.done(); - return mock.done(); - }); - - it('should create GHA workflow (including workingDirectory)', async () => { - const yamlFileName = 'openapi-file-workingdirectory'; - prompts.inject([true, 'openapi-branch-workingdirectory', yamlFileName]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => { - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1MockWithVersionHeader(version) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = 'petstore.json'; - - await expect( - run([ - spec, - '--key', - key, - '--version', - version, - '--workingDirectory', - './__tests__/__fixtures__/relative-ref-oas', - ]), - ).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledTimes(2); - expect(fs.writeFileSync).toHaveBeenNthCalledWith(2, `.github/workflows/${yamlFileName}.yml`, expect.any(String)); - - postMock.done(); - return mock.done(); - }); - - it('should reject if user says no to creating GHA workflow', async () => { - prompts.inject(['create', false]); - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }); - - const mockWithHeader = getAPIV1MockWithVersionHeader(version) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, [{ _id: 'spec1', title: 'spec1_title' }]) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', version])).rejects.toStrictEqual( - new Error( - 'GitHub Actions workflow creation cancelled. If you ever change your mind, you can run this command again with the `--github` flag.', - ), - ); - - mockWithHeader.done(); - return mock.done(); - }); - }); - - describe('command execution in GitHub Actions runner', () => { - beforeEach(() => { - beforeGHAEnv(); - }); - - afterEach(afterGHAEnv); - - it('should error out if multiple possible spec matches were found', () => { - return expect(run(['--key', key, '--version', version])).rejects.toStrictEqual( - new Error('Multiple API definitions found in current directory. Please specify file.'), - ); - }); - - it('should send proper headers in GitHub Actions CI for local spec file', async () => { - const registryUUID = getRandomRegistryId(); - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID }); - - const putMock = getAPIV1Mock({ - 'x-rdme-ci': 'GitHub Actions (test)', - 'x-readme-source': 'cli-gh', - 'x-readme-source-url': - 'https://github.com/octocat/Hello-World/blob/ffac537e6cbbf934b08745a378932722df287a53/__tests__/__fixtures__/ref-oas/petstore.json', - 'x-readme-version': version, - }) - .put(`/api/v1/api-specification/${id}`, { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = './__tests__/__fixtures__/ref-oas/petstore.json'; - - await expect(run([spec, '--key', key, '--version', version, '--id', id])).resolves.toBe(successfulUpdate(spec)); - - putMock.done(); - return mock.done(); - }); - - it('should send proper headers in GitHub Actions CI for spec hosted at URL', async () => { - const registryUUID = getRandomRegistryId(); - const spec = 'https://example.com/openapi.json'; - - const mock = getAPIV1Mock() - .post('/api/v1/api-registry', body => body.match('form-data; name="spec"')) - .reply(201, { registryUUID }); - - const exampleMock = nock('https://example.com').get('/openapi.json').reply(200, petstoreWeird); - - const putMock = getAPIV1Mock({ - 'x-rdme-ci': 'GitHub Actions (test)', - 'x-readme-source': 'cli-gh', - 'x-readme-source-url': spec, - 'x-readme-version': version, - }) - .put(`/api/v1/api-specification/${id}`, { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - await expect(run([spec, '--key', key, '--version', version, '--id', id])).resolves.toBe(successfulUpdate(spec)); - - putMock.done(); - exampleMock.done(); - return mock.done(); - }); - - it('should contain request header with correct URL with working directory', async () => { - const registryUUID = getRandomRegistryId(); - const mock = getAPIV1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .post('/api/v1/api-registry', body => { - return body.match('form-data; name="spec"'); - }) - .reply(201, { registryUUID, spec: { openapi: '3.0.0' } }) - .get('/api/v1/api-specification') - .basicAuth({ user: key }) - .reply(200, []); - - const postMock = getAPIV1Mock({ - 'x-rdme-ci': 'GitHub Actions (test)', - 'x-readme-source': 'cli-gh', - 'x-readme-source-url': - 'https://github.com/octocat/Hello-World/blob/ffac537e6cbbf934b08745a378932722df287a53/__tests__/__fixtures__/relative-ref-oas/petstore.json', - 'x-readme-version': version, - }) - .post('/api/v1/api-specification', { registryUUID }) - .basicAuth({ user: key }) - .reply(201, { _id: 1 }, { location: exampleRefLocation }); - - const spec = 'petstore.json'; - - await expect( - run([ - spec, - '--key', - key, - '--version', - version, - '--workingDirectory', - './__tests__/__fixtures__/relative-ref-oas', - ]), - ).resolves.toBe(successfulUpload(spec)); - - after(); - - postMock.done(); - return mock.done(); - }); - }); -}); diff --git a/__tests__/lib/prompts.test.ts b/__tests__/lib/prompts.test.ts index d1dba0a90..52d3bbf23 100644 --- a/__tests__/lib/prompts.test.ts +++ b/__tests__/lib/prompts.test.ts @@ -15,70 +15,7 @@ const versionlist = [ }, ]; -const specList = [ - { - _id: 'spec1', - title: 'spec1_title', - }, - { - _id: 'spec2', - title: 'spec2_title', - }, -]; - -const getSpecs = () => { - return { - body: [ - { - _id: 'spec3', - title: 'spec3_title', - }, - ], - } as unknown as Promise; -}; - describe('prompt test bed', () => { - describe('createOasPrompt()', () => { - it('should return a create option if selected', async () => { - prompts.inject(['create']); - - const answer = await promptTerminal( - promptHandler.createOasPrompt( - [ - { - _id: '1234', - title: 'buster', - }, - ], - {}, - 1, - null, - ), - ); - - expect(answer).toStrictEqual({ option: 'create' }); - }); - - it('should return specId if user chooses to update file', async () => { - prompts.inject(['update', 'spec1']); - - const parsedDocs = { - next: { - page: 2, - url: '', - }, - prev: { - page: 1, - url: '', - }, - }; - - const answer = await promptTerminal(promptHandler.createOasPrompt(specList, parsedDocs, 1, getSpecs)); - - expect(answer).toStrictEqual({ option: 'spec1' }); - }); - }); - describe('versionPrompt()', () => { it('should allow user to choose a fork if flag is not passed (creating version)', async () => { prompts.inject(['1', true, true]); diff --git a/package-lock.json b/package-lock.json index 8b13deecf..20107fb03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "simple-git": "^3.19.1", "string-argv": "^0.3.2", "table": "^6.8.1", - "tmp-promise": "^3.0.2", "toposort": "^2.0.2", "undici": "^5.28.4", "validator": "^13.7.0" @@ -16976,24 +16975,6 @@ "node": ">=0.6.0" } }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/tmp-promise/node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index ef199da1a..bd7fd3ca8 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "simple-git": "^3.19.1", "string-argv": "^0.3.2", "table": "^6.8.1", - "tmp-promise": "^3.0.2", "toposort": "^2.0.2", "undici": "^5.28.4", "validator": "^13.7.0" diff --git a/src/commands/openapi/index.ts b/src/commands/openapi/index.ts deleted file mode 100644 index 3d12fabe7..000000000 --- a/src/commands/openapi/index.ts +++ /dev/null @@ -1,353 +0,0 @@ -import type { OpenAPIPromptOptions } from '../../lib/prompts.js'; - -import { Args, Flags } from '@oclif/core'; -import chalk from 'chalk'; -import ora from 'ora'; -import parse from 'parse-link-header'; - -import BaseCommand from '../../lib/baseCommand.js'; -import { githubFlag, keyFlag, titleFlag, versionFlag, workingDirectoryFlag } from '../../lib/flags.js'; -import { info, oraOptions, warn } from '../../lib/logger.js'; -import prepareOas from '../../lib/prepareOas.js'; -import * as promptHandler from '../../lib/prompts.js'; -import promptTerminal from '../../lib/promptWrapper.js'; -import { cleanAPIv1Headers, handleAPIv1Res, readmeAPIv1Fetch } from '../../lib/readmeAPIFetch.js'; -import streamSpecToRegistry from '../../lib/streamSpecToRegistry.js'; -import { getProjectVersion } from '../../lib/versionSelect.js'; - -export default class OpenAPICommand extends BaseCommand { - static summary = 'Upload, or resync, your OpenAPI/Swagger definition to ReadMe.'; - - static description = - "Locates your API definition (if you don't supply one), validates it, and then syncs it to your API reference on ReadMe."; - - // needed for unit tests, even though we also specify this in src/index.ts - static id = 'openapi' as const; - - static state = 'deprecated'; - - static deprecationOptions = { - message: `\`rdme ${this.id}\` is deprecated and v10 will have a replacement command that supports ReadMe Refactored. For more information, please visit our migration guide: https://github.com/readmeio/rdme/tree/v9/documentation/migration-guide.md`, - }; - - static args = { - spec: Args.string({ description: 'A file/URL to your API definition' }), - }; - - static flags = { - key: keyFlag, - version: versionFlag, - id: Flags.string({ - description: - "Unique identifier for your API definition. Use this if you're re-uploading an existing API definition.", - }), - title: titleFlag, - workingDirectory: workingDirectoryFlag, - github: githubFlag, - dryRun: Flags.boolean({ - description: 'Runs the command without creating/updating any API Definitions in ReadMe. Useful for debugging.', - }), - useSpecVersion: Flags.boolean({ - description: - 'Uses the version listed in the `info.version` field in the API definition for the project version parameter.', - }), - raw: Flags.boolean({ description: 'Return the command results as a JSON object instead of a pretty output.' }), - create: Flags.boolean({ - description: 'Bypasses the create/update prompt and creates a new API definition in ReadMe.', - exclusive: ['update'], // this prevents `--create` and `--update` from being used simultaneously - }), - update: Flags.boolean({ - description: - "Bypasses the create/update prompt and automatically updates an existing API definition in ReadMe. Note that this flag only works if there's only one API definition associated with the current version.", - summary: 'Bypasses the create/update prompt and automatically updates an existing API definition in ReadMe.', - }), - }; - - static examples = [ - { - description: - 'This will upload the API definition at the given URL or path to your project and return an ID and URL for you to later update your file, and view it in the client:', - command: '<%= config.bin %> <%= command.id %> [url-or-local-path-to-file]', - }, - { - description: - 'You can omit the file name and `rdme` will scan your working directory (and any subdirectories) for OpenAPI/Swagger files. This approach will provide you with CLI prompts, so we do not recommend this technique in CI environments.', - command: '<%= config.bin %> <%= command.id %>', - }, - { - description: - 'If you want to bypass the prompt to create or update an API definition, you can pass the `--create` flag:', - command: '<%= config.bin %> <%= command.id %> [url-or-local-path-to-file] --version={project-version} --create', - }, - { - description: - 'This will edit (re-sync) an existing API definition (identified by `--id`) within your ReadMe project. **This is the recommended approach for usage in CI environments.**', - command: '<%= config.bin %> <%= command.id %> [url-or-local-path-to-file] --id={existing-api-definition-id}', - }, - { - description: - "Alternatively, you can include a version flag, which specifies the target version for your file's destination. This approach will provide you with CLI prompts, so we do not recommend this technique in CI environments.", - command: '<%= config.bin %> <%= command.id %> [url-or-local-path-to-file] --id={existing-api-definition-id}', - }, - { - description: - "If you wish to programmatically access any of this script's results (such as the API definition ID or the link to the corresponding docs in your dashboard), supply the `--raw` flag and the command will return a JSON output:", - command: '<%= config.bin %> <%= command.id %> openapi.json --id={existing-api-definition-id} --raw', - }, - { - description: - 'You can also pass in a file in a subdirectory (we recommend running the CLI from the root of your repository if possible):', - command: '<%= config.bin %> <%= command.id %> example-directory/petstore.json', - }, - { - description: - 'By default, `<%= config.bin %>` bundles all references with paths based on the directory that it is being run in. You can override the working directory using the `--workingDirectory` option, which can be helpful for bundling certain external references:', - command: '<%= config.bin %> <%= command.id %> petstore.json --workingDirectory=[path to directory]', - }, - { - description: - 'If you wish to use the version specified in the `info.version` field of your OpenAPI definition, you can pass the `--useSpecVersion` option. So if the the `info.version` field was `1.2.3`, this is equivalent to passing `--version=1.2.3`.', - command: '<%= config.bin %> <%= command.id %> [url-or-local-path-to-file] --useSpecVersion', - }, - { - description: - "If there's only one API definition for the given project version to update, you can use the `--update` flag and it will select it without any prompts:", - command: '<%= config.bin %> <%= command.id %> [url-or-local-path-to-file] --version={project-version} --update', - }, - ]; - - async run() { - const { spec } = this.args; - const { dryRun, key, id, create, raw, title, useSpecVersion, version, workingDirectory, update } = this.flags; - - let selectedVersion = version; - let isUpdate: boolean; - const spinner = ora({ ...oraOptions() }); - /** - * The `version` and `update` parameters are not typically ones we'd want to include - * in GitHub Actions workflow files, so we're going to collect them in this object. - */ - const ignoredGHAParameters: Partial = { version: undefined, update: false }; - - if (dryRun) { - warn('🎭 dry run option detected! No API definitions will be created or updated in ReadMe.'); - } - - if (workingDirectory) { - const previousWorkingDirectory = process.cwd(); - process.chdir(workingDirectory); - this.debug(`switching working directory from ${previousWorkingDirectory} to ${process.cwd()}`); - } - - if (version && id) { - warn("We'll be using the version associated with the `--id` option, so the `--version` option will be ignored."); - } - - if (create && id) { - warn("We'll be using the `--create` option, so the `--id` parameter will be ignored."); - } - - if (update && id) { - warn( - "We'll be updating the API definition associated with the `--id` parameter, so the `--update` parameter will be ignored.", - ); - } - - const { preparedSpec, specFileType, specPath, specType, specVersion } = await prepareOas(spec, 'openapi', { - title, - }); - - if (useSpecVersion) { - info(`Using the version specified in your API definition for your ReadMe project version (${specVersion})`); - selectedVersion = specVersion; - } - - if (create || !id) { - selectedVersion = await getProjectVersion(selectedVersion, key); - } - - this.debug(`selectedVersion: ${selectedVersion}`); - - const success = async (data: Response) => { - const message = !isUpdate - ? `You've successfully uploaded a new ${specType} file to your ReadMe project!` - : `You've successfully updated an existing ${specType} file on your ReadMe project!`; - - const body = await handleAPIv1Res(data, false); - - const output = { - commandType: isUpdate ? 'update' : 'create', - docs: data.headers.get('location'), - // eslint-disable-next-line no-underscore-dangle - id: body._id, - specPath, - specType, - version: selectedVersion, - }; - - const prettyOutput = [ - message, - '', - `\t${chalk.green(output.docs)}`, - '', - `To update your ${specType} definition, run the following:`, - '', - `\t${chalk.green(`rdme openapi ${specPath} --key= --id=${output.id}`)}`, - ].join('\n'); - - return this.runCreateGHAHook({ - parsedOpts: { - ...this.flags, - spec: specPath, - // eslint-disable-next-line no-underscore-dangle - id: body._id, - version: selectedVersion, - ...ignoredGHAParameters, - }, - result: raw ? JSON.stringify(output, null, 2) : prettyOutput, - }); - }; - - const error = (res: Response) => { - return handleAPIv1Res(res).catch(err => { - // If we receive an APIv1Error, no changes needed! Throw it as is. - if (err.name === 'APIv1Error') { - throw err; - } - - // If we receive certain text responses, it's likely a 5xx error from our server. - if ( - typeof err === 'string' && - (err.includes('Application Error') || // Heroku error - err.includes('520: Web server is returning an unknown error')) // Cloudflare error - ) { - throw new Error( - "We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks.", - ); - } - - // As a fallback, we throw a more generic error. - throw new Error( - `Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at ${chalk.underline( - 'support@readme.io', - )}.`, - ); - }); - }; - - const registryUUID = await streamSpecToRegistry(preparedSpec); - - const options: RequestInit = { - headers: cleanAPIv1Headers( - key, - selectedVersion, - new Headers({ Accept: 'application/json', 'Content-Type': 'application/json' }), - ), - body: JSON.stringify({ registryUUID }), - }; - - function createSpec() { - if (dryRun) { - return `🎭 dry run! The API Definition located at ${specPath} will be created for this project version: ${selectedVersion}`; - } - - options.method = 'post'; - spinner.start('Creating your API docs in ReadMe...'); - return readmeAPIv1Fetch('/api/v1/api-specification', options, { - filePath: specPath, - fileType: specFileType, - }).then(res => { - if (res.ok) { - spinner.succeed(`${spinner.text} done! đŸĻ‰`); - return success(res); - } - spinner.fail(); - return error(res); - }); - } - - function updateSpec(specId: string) { - if (dryRun) { - return `🎭 dry run! The API Definition located at ${specPath} will update this API Definition ID: ${specId}`; - } - - isUpdate = true; - options.method = 'put'; - spinner.start('Updating your API docs in ReadMe...'); - return readmeAPIv1Fetch(`/api/v1/api-specification/${specId}`, options, { - filePath: specPath, - fileType: specFileType, - }).then(res => { - if (res.ok) { - spinner.succeed(`${spinner.text} done! đŸĻ‰`); - return success(res); - } - spinner.fail(); - return error(res); - }); - } - - /* - Create a new OAS file in Readme: - - Enter flow if user does not pass an id as cli arg - - Check to see if any existing files exist with a specific version - - If none exist, default to creating a new instance of a spec - - If found, prompt user to either create a new spec or update an existing one - */ - - function getSpecs(url: string) { - if (url) { - return readmeAPIv1Fetch(url, { - method: 'get', - headers: cleanAPIv1Headers(key, selectedVersion), - }); - } - - throw new Error( - 'There was an error retrieving your list of API definitions. Please get in touch with us at support@readme.io', - ); - } - - if (create) { - ignoredGHAParameters.id = undefined; - delete ignoredGHAParameters.version; - return createSpec(); - } - - if (!id) { - this.debug('no id parameter, retrieving list of API specs'); - const apiSettings = await getSpecs('/api/v1/api-specification'); - - const totalPages = Math.ceil(parseInt(apiSettings.headers.get('x-total-count') || '0', 10) / 10); - const parsedDocs = parse(apiSettings.headers.get('link')); - this.debug(`total pages: ${totalPages}`); - this.debug(`pagination result: ${JSON.stringify(parsedDocs)}`); - - const apiSettingsBody = await handleAPIv1Res(apiSettings); - if (!apiSettingsBody.length) return createSpec(); - - if (update) { - if (apiSettingsBody.length > 1) { - throw new Error( - `The \`--update\` option cannot be used when there's more than one API definition available (found ${apiSettingsBody.length}).`, - ); - } - const { _id: specId } = apiSettingsBody[0]; - return updateSpec(specId); - } - - const { option }: { option: OpenAPIPromptOptions } = await promptTerminal( - promptHandler.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs), - ); - this.debug(`selection result: ${option}`); - - return option === 'create' ? createSpec() : updateSpec(option); - } - - /* - Update an existing OAS file in Readme: - - Enter flow if user passes an id as cli arg - */ - return updateSpec(id); - } -} diff --git a/src/index.ts b/src/index.ts index 6039677ad..054a41a82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ import LoginCommand from './commands/login.js'; import LogoutCommand from './commands/logout.js'; import OpenCommand from './commands/open.js'; import OpenAPIConvertCommand from './commands/openapi/convert.js'; -import OpenAPICommand from './commands/openapi/index.js'; import OpenAPIInspectCommand from './commands/openapi/inspect.js'; import OpenAPIReduceCommand from './commands/openapi/reduce.js'; import OpenAPIValidateCommand from './commands/openapi/validate.js'; @@ -50,7 +49,6 @@ export const COMMANDS = { logout: LogoutCommand, open: OpenCommand, - openapi: OpenAPICommand, 'openapi:convert': OpenAPIConvertCommand, 'openapi:inspect': OpenAPIInspectCommand, 'openapi:reduce': OpenAPIReduceCommand, diff --git a/src/lib/prompts.ts b/src/lib/prompts.ts index ff1a989a8..97606b31a 100644 --- a/src/lib/prompts.ts +++ b/src/lib/prompts.ts @@ -13,8 +13,6 @@ interface Spec { title: string; } -export type OpenAPIPromptOptions = 'create' | 'update'; - type SpecList = Spec[]; interface ParsedDocs { @@ -58,81 +56,6 @@ function specOptions( return specs; } -const updateOasPrompt = ( - specList: SpecList, - parsedDocs: ParsedDocs | null, - currPage: number, - totalPages: number, - getSpecs: (url: string) => Promise, -): PromptObject<'specId'>[] => [ - { - type: 'select', - name: 'specId', - message: 'Select your desired file to update', - choices: specOptions(specList, parsedDocs, currPage, totalPages), - async format(spec: string) { - if (spec === 'prev') { - try { - const newSpecs = await getSpecs(`${parsedDocs?.prev?.url || ''}`); - const newParsedDocs = parse(newSpecs.headers.get('link')); - const newSpecList = await handleAPIv1Res(newSpecs); - const { specId }: { specId: string } = await promptTerminal( - updateOasPrompt(newSpecList, newParsedDocs, currPage - 1, totalPages, getSpecs), - ); - return specId; - } catch (e) { - debug(`error retrieving previous specs: ${e.message}`); - return null; - } - } else if (spec === 'next') { - try { - const newSpecs = await getSpecs(`${parsedDocs?.next?.url || ''}`); - const newParsedDocs = parse(newSpecs.headers.get('link')); - const newSpecList = await handleAPIv1Res(newSpecs); - const { specId }: { specId: string } = await promptTerminal( - updateOasPrompt(newSpecList, newParsedDocs, currPage + 1, totalPages, getSpecs), - ); - return specId; - } catch (e) { - debug(`error retrieving next specs: ${e.message}`); - return null; - } - } - - return spec; - }, - }, -]; - -export function createOasPrompt( - specList: SpecList, - parsedDocs: ParsedDocs | null, - totalPages: number, - getSpecs: (url: string) => Promise, -): PromptObject<'option'>[] { - return [ - { - type: 'select', - name: 'option', - message: 'Would you like to update an existing OAS file or create a new one?', - choices: [ - { title: 'Update existing', value: 'update' }, - { title: 'Create a new spec', value: 'create' }, - ], - async format(picked: OpenAPIPromptOptions) { - if (picked === 'update') { - const { specId }: { specId: string } = await promptTerminal( - updateOasPrompt(specList, parsedDocs, 1, totalPages, getSpecs), - ); - return specId; - } - - return picked; - }, - }, - ]; -} - /** * Series of prompts to construct a version object, * used in our `versions create` and `versions update` commands diff --git a/src/lib/streamSpecToRegistry.ts b/src/lib/streamSpecToRegistry.ts deleted file mode 100644 index e494d4c9f..000000000 --- a/src/lib/streamSpecToRegistry.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fs from 'node:fs'; - -import ora from 'ora'; -import { file as tmpFile } from 'tmp-promise'; - -import { debug, oraOptions } from './logger.js'; -import { handleAPIv1Res, readmeAPIv1Fetch } from './readmeAPIFetch.js'; - -/** - * Uploads a spec to the API registry for usage in ReadMe - * - * @returns a UUID in the API registry - */ -export default async function streamSpecToRegistry( - /** - * path to a bundled/validated spec file - */ - spec: string, -): Promise { - const spinner = ora({ text: 'Staging your API definition for upload...', ...oraOptions() }).start(); - // Create a temporary file to write the bundled spec to, - // which we will then stream into the form data body - const { path } = await tmpFile({ prefix: 'rdme-openapi-', postfix: '.json' }); - debug(`creating temporary file at ${path}`); - await fs.writeFileSync(path, spec); - const stream = fs.createReadStream(path); - - debug('file and stream created, streaming into form data payload'); - const formData = new FormData(); - formData.append('spec', { - type: 'application/json', - name: 'openapi.json', - [Symbol.toStringTag]: 'File', - stream() { - return stream; - }, - }); - - const options = { - body: formData, - headers: { - Accept: 'application/json', - }, - method: 'POST', - }; - - return readmeAPIv1Fetch('/api/v1/api-registry', options) - .then(handleAPIv1Res) - .then(body => { - spinner.stop(); - return body.registryUUID; - }) - .catch(e => { - spinner.fail(); - throw e; - }); -}