diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3135c2f7..11326b9b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,17 +103,6 @@ jobs: if: ${{ steps.openapi-validate-fail.outcome == 'success' }} run: echo "::error::Expected validation in previous step to fail" && exit 1 - # Docs: https://rdme-test.readme.io - - name: Run `openapi` command - uses: ./rdme-repo/ - with: - rdme: openapi oas-examples-repo/3.1/json/petstore.json --key=${{ secrets.RDME_TEST_PROJECT_API_KEY }} --id=${{ secrets.RDME_TEST_PROJECT_API_SETTING }} - - - name: Run `openapi` command with weird arg syntax - uses: ./rdme-repo/ - with: - rdme: openapi "oas-examples-repo/3.1/json/petstore.json" --key "${{ secrets.RDME_TEST_PROJECT_API_KEY }}" --id=${{ secrets.RDME_TEST_PROJECT_API_SETTING }} - # this is a test to ensure that the rdme github action can run properly # the way that our users invoke it - name: E2E run of `openapi validate` on `next` branch @@ -121,3 +110,25 @@ jobs: if: ${{ github.ref }} == 'refs/heads/next' with: rdme: openapi validate oas-examples-repo/3.1/json/petstore.json + + # Docs: https://rdme-test.readme.io + - name: Run `openapi` command + uses: ./rdme-repo/ + with: + rdme: openapi upload oas-examples-repo/3.1/json/petstore.json --key=${{ secrets.RDME_REFACTORED_TEST_PROJECT_API_KEY }} + - name: Run `openapi` command with env var + uses: ./rdme-repo/ + with: + rdme: openapi upload oas-examples-repo/3.1/json/petstore.json + env: + RDME_API_KEY: ${{ secrets.RDME_REFACTORED_TEST_PROJECT_API_KEY }} + - name: Run `openapi` command with other env var + uses: ./rdme-repo/ + with: + rdme: openapi upload oas-examples-repo/3.1/json/petstore.json + env: + README_API_KEY: ${{ secrets.RDME_REFACTORED_TEST_PROJECT_API_KEY }} + - name: Run `openapi` command with weird arg syntax + uses: ./rdme-repo/ + with: + rdme: openapi upload "oas-examples-repo/3.1/json/petstore.json" --key "${{ secrets.RDME_REFACTORED_TEST_PROJECT_API_KEY }}" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c49ed6984..a8331530b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,7 +1,10 @@ name: Sync `documentation` directory to ReadMe # Run workflow for every push -on: push +on: + push: + branches: + - main jobs: sync: @@ -40,33 +43,7 @@ jobs: regex: false include: documentation/* - # Since this workflow file is in the `rdme` repository itself, - # we need to test the GitHub Action using the current commit. - # This step builds the `rdme` action code so we can do that. - # - # This step is not required for syncing your docs to ReadMe! - - name: Rebuild GitHub Action for testing purposes - run: npm run build:gha - - # And finally, with our updated documentation, - # we run the `rdme` GitHub Action to sync the Markdown file - # in the `documentation` directory. - # Here's the page we're syncing: https://docs.readme.com/docs/rdme - - # First we're going to perform a dry run of syncing process. - # We do this on every push to ensure that an actual sync will work properly - - name: Sync docs to ReadMe (dry run) - uses: ./ - with: - rdme: docs ./documentation --key=${{ secrets.README_DEVELOPERS_API_KEY }} --version=${{ vars.README_DEVELOPERS_MAIN_VERSION }} --dryRun - - # And finally, we perform an actual sync to ReadMe if we're on the main branch - name: Sync docs to ReadMe - if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' - # We use the `main` branch as ref for GitHub Action - # This is NOT recommended, as it can break your workflows without notice! - # We recommend specifying a fixed version, i.e. @8.0.0 - # Docs: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#example-using-versioned-actions - uses: readmeio/rdme@main + uses: readmeio/rdme@v9 with: rdme: docs ./documentation --key=${{ secrets.README_DEVELOPERS_API_KEY }} --version=${{ vars.README_DEVELOPERS_MAIN_VERSION }} diff --git a/README.md b/README.md index a3ed86942..fc2e0a0a9 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@

-With `rdme`, you can manage your API definition (we support [OpenAPI](https://spec.openapis.org/oas/v3.1.0.html), [Swagger](https://swagger.io/specification/v2/), and [Postman](https://schema.postman.com/)) and sync it to your API reference docs on ReadMe. You can also access other parts of [ReadMe's RESTful API](https://docs.readme.com/reference), including syncing Markdown documentation with your ReadMe project and managing project versions. +With `rdme`, you can manage your API definition (we support [OpenAPI](https://spec.openapis.org/oas/v3.1.0.html), [Swagger](https://swagger.io/specification/v2/), and [Postman](https://schema.postman.com/)) and sync it to your API reference docs on ReadMe. Not using ReadMe for your docs? No worries. `rdme` has a variety of tools to help you identify issues with your API definition β€” no ReadMe account required. -> [!WARNING] -> Heads up: our [new ReadMe Refactored experience](https://docs.readme.com/main/docs/welcome-to-readme-refactored) doesn’t yet support `rdme`. If your project is using the new ReadMe Refactored experience, we recommend [enabling bi-directional syncing via Git](https://docs.readme.com/main/docs/bi-directional-sync) for an even better editing experience for the technical and non-technical users on your team! +> [!NOTE] +> If you're using [ReadMe Refactored](https://docs.readme.com/main/docs/welcome-to-readme-refactored), you'll want to use `rdme@10` or later. If you're **not** using ReadMe Refactored, you'll want to use `rdme@9`. More info can be found in our [migration guide](https://github.com/readmeio/rdme/blob/next/documentation/migration-guide.md). # Table of Contents @@ -170,16 +170,15 @@ npm run build && npm run build:docs # Command Topics * [`rdme autocomplete`](documentation/commands/autocomplete.md) - Display autocomplete installation instructions. -* [`rdme categories`](documentation/commands/categories.md) - List or create categories in your ReadMe developer hub. * [`rdme changelogs`](documentation/commands/changelogs.md) - Sync Markdown files to your ReadMe project as Changelog posts. -* [`rdme custompages`](documentation/commands/custompages.md) - Sync Markdown/HTML files to your ReadMe project as Custom Pages. -* [`rdme docs`](documentation/commands/docs.md) - Sync or prune Guides pages in your ReadMe developer hub. * [`rdme help`](documentation/commands/help.md) - Display help for rdme. * [`rdme login`](documentation/commands/login.md) - Login to a ReadMe project. * [`rdme logout`](documentation/commands/logout.md) - Logs the currently authenticated user out of ReadMe. * [`rdme openapi`](documentation/commands/openapi.md) - Manage your API definition (e.g., syncing, validation, analysis, conversion, etc.). Supports OpenAPI, Swagger, and Postman collections, in either JSON or YAML formats. -* [`rdme versions`](documentation/commands/versions.md) - Manage your documentation versions. * [`rdme whoami`](documentation/commands/whoami.md) - Displays the current user and project authenticated with ReadMe. + +> [!IMPORTANT] +> You'll notice that several previous `rdme` commands are no longer present. That's because this version is for projects that use [ReadMe Refactored](https://docs.readme.com/main/docs/welcome-to-readme-refactored) and [bi-directional syncing](https://docs.readme.com/main/docs/bi-directional-sync) is the recommended approach for most workflows previously managed via `rdme`. See more in [our migration guide](./documentation/migration-guide.md). diff --git a/__tests__/commands/categories/create.test.ts b/__tests__/commands/categories/create.test.ts deleted file mode 100644 index 8b16d38a7..000000000 --- a/__tests__/commands/categories/create.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import nock from 'nock'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/categories/create.js'; -import { getAPIv1Mock, getAPIv1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme categories create', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => nock.cleanAll()); - - it('should error if no title provided', () => { - return expect(run(['--key', key])).rejects.toThrow('Missing 1 required arg:\ntitle'); - }); - - it('should error if categoryType is blank', () => { - return expect(run(['--key', key, 'Test Title'])).rejects.toThrow('Missing required flag categoryType'); - }); - - it('should error if categoryType is not `guide` or `reference`', () => { - return expect(run(['--key', key, 'Test Title', '--categoryType', 'test'])).rejects.toThrow( - 'Expected --categoryType=test to be one of: guide, reference', - ); - }); - - it('should create a new category if the title and type do not match and preventDuplicates=true', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .persist() - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ title: 'Existing Category', slug: 'existing-category', type: 'guide' }], { - 'x-total-count': '1', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/categories') - .basicAuth({ user: key }) - .reply(201, { title: 'New Category', slug: 'new-category', type: 'guide', id: '123' }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run(['New Category', '--categoryType', 'guide', '--key', key, '--version', '1.0.0', '--preventDuplicates']), - ).resolves.toBe("🌱 successfully created 'New Category' with a type of 'guide' and an id of '123'"); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should create a new category if the title matches but the type does not match and preventDuplicates=true', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .persist() - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ title: 'Category', slug: 'category', type: 'guide' }], { - 'x-total-count': '1', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/categories') - .basicAuth({ user: key }) - .reply(201, { title: 'Category', slug: 'category', type: 'reference', id: '123' }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run(['--categoryType', 'reference', '--key', key, '--version', '1.0.0', '--preventDuplicates', 'Category']), - ).resolves.toBe("🌱 successfully created 'Category' with a type of 'reference' and an id of '123'"); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should create a new category if the title and type match and preventDuplicates=false', async () => { - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/categories') - .basicAuth({ user: key }) - .reply(201, { title: 'Category', slug: 'category', type: 'reference', id: '123' }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['Category', '--categoryType', 'guide', '--key', key, '--version', '1.0.0'])).resolves.toBe( - "🌱 successfully created 'Category' with a type of 'reference' and an id of '123'", - ); - - postMock.done(); - versionMock.done(); - }); - - it('should not create a new category if the title and type match and preventDuplicates=true', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .persist() - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ title: 'Category', slug: 'category', type: 'guide', id: '123' }], { - 'x-total-count': '1', - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run(['Category', '--categoryType', 'guide', '--key', key, '--version', '1.0.0', '--preventDuplicates']), - ).rejects.toStrictEqual( - new Error( - "The 'Category' category with a type of 'guide' already exists with an id of '123'. A new category was not created.", - ), - ); - - getMock.done(); - versionMock.done(); - }); - - it('should not create a new category if the non case sensitive title and type match and preventDuplicates=true', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .persist() - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ title: 'Category', slug: 'category', type: 'guide', id: '123' }], { - 'x-total-count': '1', - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run(['Category', '--categoryType', 'guide', '--key', key, '--version', '1.0.0', '--preventDuplicates']), - ).rejects.toStrictEqual( - new Error( - "The 'Category' category with a type of 'guide' already exists with an id of '123'. A new category was not created.", - ), - ); - - getMock.done(); - versionMock.done(); - }); -}); diff --git a/__tests__/commands/categories/index.test.ts b/__tests__/commands/categories/index.test.ts deleted file mode 100644 index f3ebb3b54..000000000 --- a/__tests__/commands/categories/index.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import nock from 'nock'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/categories/index.js'; -import { getAPIv1Mock, getAPIv1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme categories', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => nock.cleanAll()); - - it('should return all categories for a single page', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .persist() - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ title: 'One Category', slug: 'one-category', type: 'guide' }], { - 'x-total-count': '1', - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, '--version', '1.0.0'])).resolves.toBe( - JSON.stringify([{ title: 'One Category', slug: 'one-category', type: 'guide' }], null, 2), - ); - - getMock.done(); - versionMock.done(); - }); - - it('should return all categories for multiple pages', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .persist() - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ title: 'One Category', slug: 'one-category', type: 'guide' }], { - 'x-total-count': '21', - }) - .get('/api/v1/categories?perPage=20&page=2') - .basicAuth({ user: key }) - .reply(200, [{ title: 'Another Category', slug: 'another-category', type: 'guide' }], { - 'x-total-count': '21', - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, '--version', '1.0.0'])).resolves.toBe( - JSON.stringify( - [ - { title: 'One Category', slug: 'one-category', type: 'guide' }, - { title: 'Another Category', slug: 'another-category', type: 'guide' }, - ], - null, - 2, - ), - ); - - getMock.done(); - versionMock.done(); - }); -}); diff --git a/__tests__/commands/custompages/index.test.ts b/__tests__/commands/custompages/index.test.ts deleted file mode 100644 index a0b5061d2..000000000 --- a/__tests__/commands/custompages/index.test.ts +++ /dev/null @@ -1,350 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import chalk from 'chalk'; -import frontMatter from 'gray-matter'; -import nock from 'nock'; -import { describe, beforeAll, afterAll, beforeEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/custompages.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock } from '../../helpers/get-api-mock.js'; -import hashFileContents from '../../helpers/hash-file-contents.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const fixturesBaseDir = '__fixtures__/custompages'; -const fullFixturesDir = `${__dirname}./../../${fixturesBaseDir}`; -const key = 'API_KEY'; - -describe('rdme custompages', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterAll(() => nock.cleanAll()); - - it('should error if no path provided', () => { - return expect(run(['--key', key])).rejects.toThrow('Missing 1 required arg:\npath'); - }); - - it('should error if the argument is not a folder', () => { - return expect(run(['--key', key, 'not-a-folder'])).rejects.toStrictEqual( - new Error("Oops! We couldn't locate a file or directory at the path you provided."), - ); - }); - - it('should error if the folder contains no markdown nor HTML files', () => { - return expect(run(['--key', key, '.github/workflows'])).rejects.toStrictEqual( - new Error( - "The directory you provided (.github/workflows) doesn't contain any of the following required files: .html, .markdown, .md.", - ), - ); - }); - - describe('existing custompages', () => { - let simpleDoc; - let anotherDoc; - - beforeEach(() => { - let fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/simple-doc.md')); - simpleDoc = { - slug: 'simple-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - - fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/subdir/another-doc.md')); - anotherDoc = { - slug: 'another-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - }); - - it('should fetch custom page and merge with what is returned', () => { - expect.assertions(1); - - const getMocks = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, htmlmode: false, lastUpdatedHash: 'anOldHash' }) - .get('/api/v1/custompages/another-doc') - .basicAuth({ user: key }) - .reply(200, { slug: anotherDoc.slug, htmlmode: false, lastUpdatedHash: 'anOldHash' }); - - const updateMocks = getAPIv1Mock() - .put('/api/v1/custompages/simple-doc', { - body: simpleDoc.doc.content, - htmlmode: false, - lastUpdatedHash: simpleDoc.hash, - ...simpleDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { - slug: simpleDoc.slug, - htmlmode: false, - body: simpleDoc.doc.content, - }) - .put('/api/v1/custompages/another-doc', { - body: anotherDoc.doc.content, - htmlmode: false, - lastUpdatedHash: anotherDoc.hash, - ...anotherDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { slug: anotherDoc.slug, body: anotherDoc.doc.content, htmlmode: false }); - - return run([`./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key]).then(updatedDocs => { - // All custompages should have been updated because their hashes from the GET request were different from what they - // are currently. - expect(updatedDocs).toBe( - [ - `✏️ successfully updated 'simple-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - `✏️ successfully updated 'another-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/subdir/another-doc.md`, - ].join('\n'), - ); - - getMocks.done(); - updateMocks.done(); - }); - }); - - it('should return custom page update info for dry run', () => { - expect.assertions(1); - - const getMocks = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }) - .get('/api/v1/custompages/another-doc') - .basicAuth({ user: key }) - .reply(200, { slug: anotherDoc.slug, lastUpdatedHash: 'anOldHash' }); - - return run(['--dryRun', `./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key]).then(updatedDocs => { - // All custompages should have been updated because their hashes from the GET request were different from what they - // are currently. - expect(updatedDocs).toBe( - [ - `🎭 dry run! This will update 'simple-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/simple-doc.md with the following metadata: ${JSON.stringify( - simpleDoc.doc.data, - )}`, - `🎭 dry run! This will update 'another-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/subdir/another-doc.md with the following metadata: ${JSON.stringify( - anotherDoc.doc.data, - )}`, - ].join('\n'), - ); - - getMocks.done(); - }); - }); - - it('should not send requests for custompages that have not changed', () => { - expect.assertions(1); - - const getMocks = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }) - .get('/api/v1/custompages/another-doc') - .basicAuth({ user: key }) - .reply(200, { slug: anotherDoc.slug, lastUpdatedHash: anotherDoc.hash }); - - return run([`./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key]).then(skippedDocs => { - expect(skippedDocs).toBe( - [ - '`simple-doc` was not updated because there were no changes.', - '`another-doc` was not updated because there were no changes.', - ].join('\n'), - ); - - getMocks.done(); - }); - }); - - it('should adjust "no changes" message if in dry run', () => { - expect.assertions(1); - - const getMocks = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }) - .get('/api/v1/custompages/another-doc') - .basicAuth({ user: key }) - .reply(200, { slug: anotherDoc.slug, lastUpdatedHash: anotherDoc.hash }); - - return run(['--dryRun', `./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key]).then(skippedDocs => { - expect(skippedDocs).toBe( - [ - '🎭 dry run! `simple-doc` will not be updated because there were no changes.', - '🎭 dry run! `another-doc` will not be updated because there were no changes.', - ].join('\n'), - ); - - getMocks.done(); - }); - }); - }); - - describe('new custompages', () => { - it('should create new custom page', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/custompages', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs`, '--key', key])).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/new-docs/new-doc.md`, - ); - - getMock.done(); - postMock.done(); - }); - - it('should create new HTML custom page', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs-html/${slug}.html`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs-html/${slug}.html`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/custompages', { slug, html: doc.content, htmlmode: true, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, html: doc.content, htmlmode: true, ...doc.data, lastUpdatedHash: hash }); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs-html`, '--key', key])).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/new-docs-html/new-doc.html`, - ); - - getMock.done(); - postMock.done(); - }); - - it('should return creation info for dry run', async () => { - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - await expect(run(['--dryRun', `./__tests__/${fixturesBaseDir}/new-docs`, '--key', key])).resolves.toBe( - `🎭 dry run! This will create 'new-doc' with contents from __tests__/${fixturesBaseDir}/new-docs/new-doc.md with the following metadata: ${JSON.stringify( - doc.data, - )}`, - ); - - getMock.done(); - }); - - it('should fail if any custompages are invalid', async () => { - const folder = 'failure-docs'; - const slug = 'new-doc'; - - const errorObject = { - error: 'CUSTOMPAGE_INVALID', - message: "We couldn't save this page (Custom page title cannot be blank).", - suggestion: 'Make sure all the data is correct, and the body is valid Markdown or HTML.', - docs: 'fake-metrics-uuid', - help: "If you need help, email support@readme.io and include the following link to your API log: 'fake-metrics-uuid'.", - }; - - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${folder}/${slug}.md`))); - - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${folder}/${slug}.md`))); - - const getMocks = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMocks = getAPIv1Mock() - .post('/api/v1/custompages', { slug, body: doc.content, htmlmode: false, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(400, errorObject); - - const fullDirectory = `__tests__/${fixturesBaseDir}/${folder}`; - - const formattedErrorObject = { - ...errorObject, - message: `Error uploading ${chalk.underline(`${fullDirectory}/${slug}.md`)}:\n\n${errorObject.message}`, - }; - - await expect(run([`./${fullDirectory}`, '--key', key])).rejects.toStrictEqual( - new APIv1Error(formattedErrorObject), - ); - - getMocks.done(); - postMocks.done(); - }); - }); - - describe('slug metadata', () => { - it('should use provided slug', async () => { - const slug = 'new-doc-slug'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${doc.data.slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/custompages', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug: doc.data.slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - await expect(run([`./__tests__/${fixturesBaseDir}/slug-docs`, '--key', key])).resolves.toBe( - `🌱 successfully created 'marc-actually-wrote-a-test' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/slug-docs/new-doc-slug.md`, - ); - - getMock.done(); - postMock.done(); - }); - }); -}); diff --git a/__tests__/commands/custompages/single.test.ts b/__tests__/commands/custompages/single.test.ts deleted file mode 100644 index e255b3d6c..000000000 --- a/__tests__/commands/custompages/single.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import chalk from 'chalk'; -import frontMatter from 'gray-matter'; -import nock from 'nock'; -import { describe, beforeAll, afterAll, beforeEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/custompages.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock } from '../../helpers/get-api-mock.js'; -import hashFileContents from '../../helpers/hash-file-contents.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const fixturesBaseDir = '__fixtures__/custompages'; -const fullFixturesDir = `${__dirname}./../../${fixturesBaseDir}`; -const key = 'API_KEY'; - -describe('rdme custompages (single)', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterAll(() => nock.cleanAll()); - - it('should error if no file path provided', () => { - return expect(run(['--key', key])).rejects.toThrow('Missing 1 required arg:\npath'); - }); - - it('should error if the argument is not a Markdown/HTML file', () => { - return expect(run(['--key', key, 'package.json'])).rejects.toStrictEqual( - new Error('Invalid file extension (.json). Must be one of the following: .html, .markdown, .md'), - ); - }); - - it('should error if file path cannot be found', () => { - return expect(run(['--key', key, 'non-existent-file.markdown'])).rejects.toStrictEqual( - new Error("Oops! We couldn't locate a file or directory at the path you provided."), - ); - }); - - describe('new custompages', () => { - it('should create new custom page', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/custompages', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, body: doc.content, ...doc.data }); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, '--key', key])).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from ./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, - ); - - getMock.done(); - postMock.done(); - }); - - it('should create new HTML custom page', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs-html/${slug}.html`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs-html/${slug}.html`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/custompages', { slug, html: doc.content, htmlmode: true, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, html: doc.content, htmlmode: true, ...doc.data }); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs-html/new-doc.html`, '--key', key])).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from ./__tests__/${fixturesBaseDir}/new-docs-html/new-doc.html`, - ); - - getMock.done(); - postMock.done(); - }); - - it('should return creation info for dry run', async () => { - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - await expect(run(['--dryRun', `./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, '--key', key])).resolves.toBe( - `🎭 dry run! This will create 'new-doc' with contents from ./__tests__/${fixturesBaseDir}/new-docs/new-doc.md with the following metadata: ${JSON.stringify( - doc.data, - )}`, - ); - - getMock.done(); - }); - - it('should skip if it does not contain any front matter attributes', async () => { - const filePath = `./__tests__/${fixturesBaseDir}/failure-docs/doc-sans-attributes.md`; - - await expect(run([filePath, '--key', key])).resolves.toBe( - `⏭️ no front matter attributes found for ${filePath}, skipping`, - ); - }); - - it('should fail if some other error when retrieving page slug', async () => { - const slug = 'new-doc'; - - const errorObject = { - error: 'INTERNAL_ERROR', - message: 'Unknown error (yikes)', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${slug}`) - .basicAuth({ user: key }) - .reply(500, errorObject); - - const filePath = `./__tests__/${fixturesBaseDir}/failure-docs/${slug}.md`; - - const formattedErrorObject = { - ...errorObject, - message: `Error uploading ${chalk.underline(`${filePath}`)}:\n\n${errorObject.message}`, - }; - - await expect(run([filePath, '--key', key])).rejects.toStrictEqual(new APIv1Error(formattedErrorObject)); - - getMock.done(); - }); - }); - - describe('slug metadata', () => { - it('should use provided slug', async () => { - const slug = 'new-doc-slug'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/custompages/${doc.data.slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'CUSTOMPAGE_NOTFOUND', - message: `The custom page with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/custompages', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug: doc.data.slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - await expect(run([`./__tests__/${fixturesBaseDir}/slug-docs/new-doc-slug.md`, '--key', key])).resolves.toBe( - `🌱 successfully created 'marc-actually-wrote-a-test' (ID: 1234) with contents from ./__tests__/${fixturesBaseDir}/slug-docs/new-doc-slug.md`, - ); - - getMock.done(); - postMock.done(); - }); - }); - - describe('existing custompages', () => { - let simpleDoc; - - beforeEach(() => { - const fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/simple-doc.md')); - simpleDoc = { - slug: 'simple-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - }); - - it('should fetch custom page and merge with what is returned', () => { - const getMock = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const updateMock = getAPIv1Mock() - .put('/api/v1/custompages/simple-doc', { - body: simpleDoc.doc.content, - htmlmode: false, - lastUpdatedHash: simpleDoc.hash, - ...simpleDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { - slug: simpleDoc.slug, - body: simpleDoc.doc.content, - htmlmode: false, - }); - - return run([`./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key]).then(updatedDocs => { - expect(updatedDocs).toBe( - `✏️ successfully updated 'simple-doc' with contents from ./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - ); - - getMock.done(); - updateMock.done(); - }); - }); - - it('should return custom page update info for dry run', () => { - expect.assertions(1); - - const getMock = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }); - - return run(['--dryRun', `./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key]).then( - updatedDocs => { - // All custompages should have been updated because their hashes from the GET request were different from what they - // are currently. - expect(updatedDocs).toBe( - [ - `🎭 dry run! This will update 'simple-doc' with contents from ./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md with the following metadata: ${JSON.stringify( - simpleDoc.doc.data, - )}`, - ].join('\n'), - ); - - getMock.done(); - }, - ); - }); - - it('should not send requests for custompages that have not changed', () => { - expect.assertions(1); - - const getMock = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }); - - return run([`./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key]).then(skippedDocs => { - expect(skippedDocs).toBe('`simple-doc` was not updated because there were no changes.'); - - getMock.done(); - }); - }); - - it('should adjust "no changes" message if in dry run', () => { - const getMock = getAPIv1Mock() - .get('/api/v1/custompages/simple-doc') - .basicAuth({ user: key }) - .reply(200, { slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }); - - return run(['--dryRun', `./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key]).then( - skippedDocs => { - expect(skippedDocs).toBe('🎭 dry run! `simple-doc` will not be updated because there were no changes.'); - - getMock.done(); - }, - ); - }); - }); -}); diff --git a/__tests__/commands/docs/__snapshots__/index.test.ts.snap b/__tests__/commands/docs/__snapshots__/index.test.ts.snap deleted file mode 100644 index 3f7082f4c..000000000 --- a/__tests__/commands/docs/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,124 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`rdme docs > GHA onboarding E2E tests > should create GHA workflow with version passed as opt (github flag enabled) 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/docs-test-file-github-flag.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`rdme docs > GHA onboarding E2E tests > should create GHA workflow with version passed as opt (github flag enabled) 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`docs-test-branch-github-flag\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - docs-test-branch-github-flag - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./__tests__/__fixtures__/docs/new-docs --key=\${{ secrets.README_API_KEY }} --version=1.0.0 -" -`; - -exports[`rdme docs > GHA onboarding E2E tests > should create GHA workflow with version passed in via opt 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/docs-test-file.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`rdme docs > GHA onboarding E2E tests > should create GHA workflow with version passed in via opt 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`docs-test-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - docs-test-branch - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./__tests__/__fixtures__/docs/new-docs --key=\${{ secrets.README_API_KEY }} --version=1.0.0 -" -`; - -exports[`rdme docs > GHA onboarding E2E tests > should create GHA workflow with version passed in via prompt 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/docs-test-file.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`rdme docs > GHA onboarding E2E tests > should create GHA workflow with version passed in via prompt 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`docs-test-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - docs-test-branch - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./__tests__/__fixtures__/docs/new-docs --key=\${{ secrets.README_API_KEY }} --version=1.0.1 -" -`; diff --git a/__tests__/commands/docs/__snapshots__/multiple.test.ts.snap b/__tests__/commands/docs/__snapshots__/multiple.test.ts.snap deleted file mode 100644 index e6de4e4bc..000000000 --- a/__tests__/commands/docs/__snapshots__/multiple.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`rdme docs (multiple) > should return an error message when it encounters a cycle 1`] = `[Error: Cyclic dependency, node was:{"content":"\\n# Parent Body\\n","data":{"title":"Parent","parentDocSlug":"grandparent"},"filePath":"__tests__/__fixtures__/docs/multiple-docs-cycle/parent.md","hash":"0fc832371f8e240047bfc14bc8be9e37d50c8bb8","slug":"parent"}]`; diff --git a/__tests__/commands/docs/index.test.ts b/__tests__/commands/docs/index.test.ts deleted file mode 100644 index f71d9d12c..000000000 --- a/__tests__/commands/docs/index.test.ts +++ /dev/null @@ -1,729 +0,0 @@ -/* eslint-disable no-console */ - -import fs from 'node:fs'; -import path from 'node:path'; - -import chalk from 'chalk'; -import frontMatter from 'gray-matter'; -import nock from 'nock'; -import prompts from 'prompts'; -import { describe, beforeAll, afterAll, beforeEach, afterEach, it, expect, vi, type MockInstance } from 'vitest'; - -import Command from '../../../src/commands/docs/index.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock, getAPIv1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import { after, before } from '../../helpers/get-gha-setup.js'; -import hashFileContents from '../../helpers/hash-file-contents.js'; -import { runCommandAndReturnResult, runCommandWithHooks } from '../../helpers/oclif.js'; -import { after as afterGHAEnv, before as beforeGHAEnv } from '../../helpers/setup-gha-env.js'; - -const fixturesBaseDir = '__fixtures__/docs'; -const fullFixturesDir = `${__dirname}./../../${fixturesBaseDir}`; - -const key = 'API_KEY'; -const version = '1.0.0'; -const category = 'CATEGORY_ID'; - -describe('rdme docs', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterAll(() => nock.cleanAll()); - - it('should error if no path provided', () => { - return expect(run(['--key', key, '--version', '1.0.0'])).rejects.toThrow('Missing 1 required arg:\npath'); - }); - - it('should error if the argument is not a folder', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, '--version', '1.0.0', 'not-a-folder'])).rejects.toStrictEqual( - new Error("Oops! We couldn't locate a file or directory at the path you provided."), - ); - - versionMock.done(); - }); - - it('should error if the folder contains no markdown files', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, '--version', '1.0.0', '.github/workflows'])).rejects.toStrictEqual( - new Error( - "The directory you provided (.github/workflows) doesn't contain any of the following required files: .markdown, .md.", - ), - ); - - versionMock.done(); - }); - - describe('existing docs', () => { - let simpleDoc; - let anotherDoc; - - beforeEach(() => { - let fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/simple-doc.md')); - simpleDoc = { - slug: 'simple-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - - fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/subdir/another-doc.md')); - anotherDoc = { - slug: 'another-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - }); - - it('should fetch doc and merge with what is returned', () => { - expect.assertions(1); - - const getMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }) - .get('/api/v1/docs/another-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const updateMocks = getAPIv1MockWithVersionHeader(version) - .put('/api/v1/docs/simple-doc', { - body: simpleDoc.doc.content, - lastUpdatedHash: simpleDoc.hash, - ...simpleDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { - category, - slug: simpleDoc.slug, - body: simpleDoc.doc.content, - }) - .put('/api/v1/docs/another-doc', { - body: anotherDoc.doc.content, - lastUpdatedHash: anotherDoc.hash, - ...anotherDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, body: anotherDoc.doc.content }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - return run([`./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key, '--version', version]).then( - updatedDocs => { - // All docs should have been updated because their hashes from the GET request were different from what they - // are currently. - expect(updatedDocs).toBe( - [ - `✏️ successfully updated 'simple-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - `✏️ successfully updated 'another-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/subdir/another-doc.md`, - ].join('\n'), - ); - - getMocks.done(); - updateMocks.done(); - versionMock.done(); - }, - ); - }); - - it('should return doc update info for dry run', () => { - expect.assertions(1); - - const getMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }) - .get('/api/v1/docs/another-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - return run(['--dryRun', `./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key, '--version', version]).then( - updatedDocs => { - // All docs should have been updated because their hashes from the GET request were different from what they - // are currently. - expect(updatedDocs).toBe( - [ - `🎭 dry run! This will update 'simple-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/simple-doc.md with the following metadata: ${JSON.stringify( - simpleDoc.doc.data, - )}`, - `🎭 dry run! This will update 'another-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/subdir/another-doc.md with the following metadata: ${JSON.stringify( - anotherDoc.doc.data, - )}`, - ].join('\n'), - ); - - getMocks.done(); - versionMock.done(); - }, - ); - }); - - it('should not send requests for docs that have not changed', () => { - expect.assertions(1); - - const getMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }) - .get('/api/v1/docs/another-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, lastUpdatedHash: anotherDoc.hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - return run([`./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key, '--version', version]).then( - skippedDocs => { - expect(skippedDocs).toBe( - [ - '`simple-doc` was not updated because there were no changes.', - '`another-doc` was not updated because there were no changes.', - ].join('\n'), - ); - - getMocks.done(); - versionMock.done(); - }, - ); - }); - - it('should adjust "no changes" message if in dry run', () => { - expect.assertions(1); - - const getMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }) - .get('/api/v1/docs/another-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, lastUpdatedHash: anotherDoc.hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - return run(['--dryRun', `./__tests__/${fixturesBaseDir}/existing-docs`, '--key', key, '--version', version]).then( - skippedDocs => { - expect(skippedDocs).toBe( - [ - '🎭 dry run! `simple-doc` will not be updated because there were no changes.', - '🎭 dry run! `another-doc` will not be updated because there were no changes.', - ].join('\n'), - ); - - getMocks.done(); - versionMock.done(); - }, - ); - }); - }); - - describe('new docs', () => { - it('should create new doc', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs`, '--key', key, '--version', version])).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/new-docs/new-doc.md`, - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should return creation info for dry run', async () => { - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run(['--dryRun', `./__tests__/${fixturesBaseDir}/new-docs`, '--key', key, '--version', version]), - ).resolves.toBe( - `🎭 dry run! This will create 'new-doc' with contents from __tests__/${fixturesBaseDir}/new-docs/new-doc.md with the following metadata: ${JSON.stringify( - doc.data, - )}`, - ); - - getMock.done(); - versionMock.done(); - }); - - it('should fail if any docs are invalid', async () => { - const folder = 'failure-docs'; - const slug = 'new-doc'; - - const errorObject = { - error: 'DOC_INVALID', - message: "We couldn't save this doc (Path `category` is required.).", - }; - - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${folder}/${slug}.md`))); - - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${folder}/${slug}.md`))); - - const getMocks = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMocks = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(400, errorObject); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const fullDirectory = `__tests__/${fixturesBaseDir}/${folder}`; - - const formattedErrorObject = { - ...errorObject, - message: `Error uploading ${chalk.underline(`${fullDirectory}/${slug}.md`)}:\n\n${errorObject.message}`, - }; - - await expect(run([`./${fullDirectory}`, '--key', key, '--version', version])).rejects.toStrictEqual( - new APIv1Error(formattedErrorObject), - ); - - getMocks.done(); - postMocks.done(); - versionMock.done(); - }); - }); - - describe('slug metadata', () => { - it('should use provided slug', async () => { - const slug = 'new-doc-slug'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/docs/${doc.data.slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug: doc.data.slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run([`./__tests__/${fixturesBaseDir}/slug-docs`, '--key', key, '--version', version])).resolves.toBe( - `🌱 successfully created 'marc-actually-wrote-a-test' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/slug-docs/new-doc-slug.md`, - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - }); - - describe('GHA onboarding E2E tests', () => { - let consoleInfoSpy: MockInstance; - let yamlOutput; - - const getCommandOutput = () => { - return [consoleInfoSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); - }; - - beforeEach(() => { - consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - - before((fileName, data) => { - yamlOutput = data; - }); - }); - - afterEach(() => { - after(); - - consoleInfoSpy.mockRestore(); - }); - - it('should create GHA workflow with version passed in via prompt', async () => { - expect.assertions(6); - - const altVersion = '1.0.1'; - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const versionsMock = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }, { version: altVersion }]); - - const getMock = getAPIv1MockWithVersionHeader(altVersion) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1MockWithVersionHeader(altVersion) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { _id: id, slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const fileName = 'docs-test-file'; - prompts.inject([altVersion, true, 'docs-test-branch', fileName]); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs`, '--key', key])).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${fileName}.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 created '${slug}' (ID: ${id}) with contents from`); - - versionsMock.done(); - getMock.done(); - postMock.done(); - }); - - it('should create GHA workflow with version passed in via opt', async () => { - expect.assertions(3); - - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const fileName = 'docs-test-file'; - prompts.inject([true, 'docs-test-branch', fileName]); - - await expect( - run([`./__tests__/${fixturesBaseDir}/new-docs`, '--key', key, '--version', version]), - ).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${fileName}.yml`, expect.any(String)); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should create GHA workflow with version passed as opt (github flag enabled)', async () => { - expect.assertions(3); - - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const fileName = 'docs-test-file-github-flag'; - prompts.inject(['docs-test-branch-github-flag', fileName]); - - await expect( - run([`./__tests__/${fixturesBaseDir}/new-docs`, '--github', '--key', key, '--version', version]), - ).resolves.toMatchSnapshot(); - - expect(yamlOutput).toMatchSnapshot(); - expect(fs.writeFileSync).toHaveBeenCalledWith(`.github/workflows/${fileName}.yml`, expect.any(String)); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should reject if user says no to creating GHA workflow', async () => { - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - prompts.inject([false]); - - await expect( - run([`./__tests__/${fixturesBaseDir}/new-docs`, '--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.', - ), - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - }); - - describe('command execution in GitHub Actions runner', () => { - beforeEach(() => { - beforeGHAEnv(); - }); - - afterEach(afterGHAEnv); - - it('should sync new docs directory with correct headers', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - 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__/docs/new-docs/new-doc.md', - 'x-readme-version': version, - }) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run([`./__tests__/${fixturesBaseDir}/new-docs`, '--key', key, '--version', version])).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/new-docs/new-doc.md`, - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should sync existing docs directory with correct headers', () => { - let fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/simple-doc.md')); - const simpleDoc = { - slug: 'simple-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - - fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/subdir/another-doc.md')); - const anotherDoc = { - slug: 'another-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - - expect.assertions(1); - - const getMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }) - .get('/api/v1/docs/another-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const firstUpdateMock = 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__/docs/existing-docs/simple-doc.md', - 'x-readme-version': version, - }) - .put('/api/v1/docs/simple-doc', { - body: simpleDoc.doc.content, - lastUpdatedHash: simpleDoc.hash, - ...simpleDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { - category, - slug: simpleDoc.slug, - body: simpleDoc.doc.content, - }); - - const secondUpdateMock = 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__/docs/existing-docs/subdir/another-doc.md', - 'x-readme-version': version, - }) - .put('/api/v1/docs/another-doc', { - body: anotherDoc.doc.content, - lastUpdatedHash: anotherDoc.hash, - ...anotherDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { category, slug: anotherDoc.slug, body: anotherDoc.doc.content }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - return run([`__tests__/${fixturesBaseDir}/existing-docs`, '--key', key, '--version', version]).then( - updatedDocs => { - // All docs should have been updated because their hashes from the GET request were different from what they - // are currently. - expect(updatedDocs).toBe( - [ - `✏️ successfully updated 'simple-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - `✏️ successfully updated 'another-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/subdir/another-doc.md`, - ].join('\n'), - ); - - getMocks.done(); - firstUpdateMock.done(); - secondUpdateMock.done(); - versionMock.done(); - }, - ); - }); - }); - - describe('rdme guides', () => { - it('should error if no path provided', async () => { - return expect( - (await runCommandWithHooks(['guides', '--key', key, '--version', '1.0.0'])).error.message, - ).toContain('Missing 1 required arg:\npath'); - }); - }); -}); diff --git a/__tests__/commands/docs/multiple.test.ts b/__tests__/commands/docs/multiple.test.ts deleted file mode 100644 index 40c0a29de..000000000 --- a/__tests__/commands/docs/multiple.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import frontMatter from 'gray-matter'; -import nock from 'nock'; -import { describe, beforeAll, afterAll, it, expect } from 'vitest'; - -import Command from '../../../src/commands/docs/index.js'; -import { getAPIv1Mock, getAPIv1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import hashFileContents from '../../helpers/hash-file-contents.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const fixturesBaseDir = '__fixtures__/docs'; -const fullFixturesDir = `${__dirname}./../../${fixturesBaseDir}`; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme docs (multiple)', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterAll(() => nock.cleanAll()); - - it('should upload parent docs first', async () => { - const dir = 'multiple-docs'; - const slugs = ['grandparent', 'parent', 'child', 'friend']; - let id = 1234; - - const mocks = slugs.flatMap(slug => { - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`))); - - return [ - getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }), - getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - // eslint-disable-next-line no-plusplus - .reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }), - ]; - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const promise = run([`./__tests__/${fixturesBaseDir}/${dir}`, '--key', key, '--version', version]); - - await expect(promise).resolves.toStrictEqual( - [ - `🌱 successfully created 'friend' (ID: 1237) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`, - `🌱 successfully created 'grandparent' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/grandparent.md`, - `🌱 successfully created 'parent' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/parent.md`, - `🌱 successfully created 'child' (ID: 1236) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`, - ].join('\n'), - ); - - mocks.forEach(mock => mock.done()); - versionMock.done(); - }); - - it('should upload docs with parent doc ids first', async () => { - const dir = 'docs-with-parent-ids'; - const slugs = ['child', 'friend', 'with-parent-doc', 'parent']; - let id = 1234; - - const mocks = slugs.flatMap(slug => { - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`))); - - return [ - getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }), - getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - // eslint-disable-next-line no-plusplus - .reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }), - ]; - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const promise = run([`./__tests__/${fixturesBaseDir}/${dir}`, '--key', key, '--version', version]); - - await expect(promise).resolves.toStrictEqual( - [ - `🌱 successfully created 'with-parent-doc' (ID: 1236) with contents from __tests__/${fixturesBaseDir}/${dir}/with-parent-doc.md`, - `🌱 successfully created 'friend' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`, - `🌱 successfully created 'parent' (ID: 1237) with contents from __tests__/${fixturesBaseDir}/${dir}/parent.md`, - `🌱 successfully created 'child' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`, - ].join('\n'), - ); - - mocks.forEach(mock => mock.done()); - versionMock.done(); - }); - - it('should upload child docs without the parent', async () => { - const dir = 'multiple-docs-no-parents'; - const slugs = ['child', 'friend']; - let id = 1234; - - const mocks = slugs.flatMap(slug => { - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`))); - - return [ - getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }), - getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - // eslint-disable-next-line no-plusplus - .reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }), - ]; - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const promise = run([`./__tests__/${fixturesBaseDir}/${dir}`, '--key', key, '--version', version]); - - await expect(promise).resolves.toStrictEqual( - [ - `🌱 successfully created 'child' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`, - `🌱 successfully created 'friend' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`, - ].join('\n'), - ); - - mocks.forEach(mock => mock.done()); - versionMock.done(); - }); - - it('should return an error message when it encounters a cycle', async () => { - const dir = 'multiple-docs-cycle'; - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const promise = run([`./__tests__/${fixturesBaseDir}/${dir}`, '--key', key, '--version', version]); - - await expect(promise).rejects.toMatchSnapshot(); - versionMock.done(); - }); -}); diff --git a/__tests__/commands/docs/prune.test.ts b/__tests__/commands/docs/prune.test.ts deleted file mode 100644 index 162e5cb7e..000000000 --- a/__tests__/commands/docs/prune.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import nock from 'nock'; -import prompts from 'prompts'; -import { describe, beforeAll, afterAll, it, expect } from 'vitest'; - -import Command from '../../../src/commands/docs/prune.js'; -import { getAPIv1Mock, getAPIv1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult, runCommandWithHooks } from '../../helpers/oclif.js'; - -const fixturesBaseDir = '__fixtures__/docs'; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme docs prune', () => { - const folder = `./__tests__/${fixturesBaseDir}/delete-docs`; - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterAll(() => nock.cleanAll()); - - it('should error if no folder provided', () => { - return expect(run(['--key', key, '--version', version])).rejects.rejects.toThrow('Missing 1 required arg:\nfolder'); - }); - - it('should error if the argument is not a folder', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, '--version', version, 'not-a-folder'])).rejects.toStrictEqual( - new Error("ENOENT: no such file or directory, scandir 'not-a-folder'"), - ); - - versionMock.done(); - }); - - it('should do nothing if the user aborted', async () => { - prompts.inject([false]); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run([folder, '--key', key, '--version', version])).rejects.toStrictEqual( - new Error('Aborting, no changes were made.'), - ); - - versionMock.done(); - }); - - it('should not ask for user confirmation if `confirm` is set to true', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const apiMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) - .get('/api/v1/categories/category1/docs') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }]) - .delete('/api/v1/docs/this-doc-should-be-missing-in-folder') - .basicAuth({ user: key }) - .reply(204, ''); - - await expect(run([folder, '--key', key, '--version', version, '--confirm'])).resolves.toBe( - 'πŸ—‘οΈ successfully deleted `this-doc-should-be-missing-in-folder`.', - ); - - apiMocks.done(); - versionMock.done(); - }); - - it('should delete doc if file is missing', async () => { - prompts.inject([true]); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const apiMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) - .get('/api/v1/categories/category1/docs') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }]) - .delete('/api/v1/docs/this-doc-should-be-missing-in-folder') - .basicAuth({ user: key }) - .reply(204, ''); - - await expect(run([folder, '--key', key, '--version', version])).resolves.toBe( - 'πŸ—‘οΈ successfully deleted `this-doc-should-be-missing-in-folder`.', - ); - - apiMocks.done(); - versionMock.done(); - }); - - it('should delete doc and its child if they are missing', async () => { - prompts.inject([true]); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const apiMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) - .get('/api/v1/categories/category1/docs') - .basicAuth({ user: key }) - .reply(200, [ - { slug: 'this-doc-should-be-missing-in-folder', children: [{ slug: 'this-child-is-also-missing' }] }, - { slug: 'some-doc' }, - ]) - .delete('/api/v1/docs/this-doc-should-be-missing-in-folder') - .basicAuth({ user: key }) - .reply(204, '') - .delete('/api/v1/docs/this-child-is-also-missing') - .basicAuth({ user: key }) - .reply(204, ''); - - await expect(run([folder, '--key', key, '--version', version])).resolves.toBe( - 'πŸ—‘οΈ successfully deleted `this-child-is-also-missing`.\nπŸ—‘οΈ successfully deleted `this-doc-should-be-missing-in-folder`.', - ); - - apiMocks.done(); - versionMock.done(); - }); - - it('should return doc delete info for dry run', async () => { - prompts.inject([true]); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - const apiMocks = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/categories?perPage=20&page=1') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' }) - .get('/api/v1/categories/category1/docs') - .basicAuth({ user: key }) - .reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }]); - - await expect(run([folder, '--key', key, '--version', version, '--dryRun'])).resolves.toBe( - '🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.', - ); - - apiMocks.done(); - versionMock.done(); - }); - - describe('rdme guides prune', () => { - it('should error if no folder provided', async () => { - return expect( - (await runCommandWithHooks(['guides', 'prune', '--key', key, '--version', version])).error.message, - ).toContain('Missing 1 required arg:\nfolder'); - }); - }); -}); diff --git a/__tests__/commands/docs/single.test.ts b/__tests__/commands/docs/single.test.ts deleted file mode 100644 index 007bd28d2..000000000 --- a/__tests__/commands/docs/single.test.ts +++ /dev/null @@ -1,443 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import chalk from 'chalk'; -import frontMatter from 'gray-matter'; -import nock from 'nock'; -import { describe, beforeAll, afterAll, beforeEach, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/docs/index.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock, getAPIv1MockWithVersionHeader } from '../../helpers/get-api-mock.js'; -import hashFileContents from '../../helpers/hash-file-contents.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; -import { after as afterGHAEnv, before as beforeGHAEnv } from '../../helpers/setup-gha-env.js'; - -const fixturesBaseDir = '__fixtures__/docs'; -const fullFixturesDir = `${__dirname}./../../${fixturesBaseDir}`; - -const key = 'API_KEY'; -const version = '1.0.0'; -const category = 'CATEGORY_ID'; - -describe('rdme docs (single)', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterAll(() => nock.cleanAll()); - - it('should error if no file path provided', () => { - return expect(run(['--key', key, '--version', version])).rejects.toThrow('Missing 1 required arg:\npath'); - }); - - it('should error if the argument is not a Markdown file', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, '--version', version, 'not-a-markdown-file'])).rejects.toStrictEqual( - new Error("Oops! We couldn't locate a file or directory at the path you provided."), - ); - - versionMock.done(); - }); - - it('should support .markdown files but error if file path cannot be found', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - await expect(run(['--key', key, '--version', version, 'non-existent-file.markdown'])).rejects.toStrictEqual( - new Error("Oops! We couldn't locate a file or directory at the path you provided."), - ); - versionMock.done(); - }); - - describe('new docs', () => { - it('should create new doc', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1MockWithVersionHeader(version) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([`./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, '--key', key, '--version', version]), - ).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from ./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should return creation info for dry run', async () => { - const slug = 'new-doc'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run(['--dryRun', `./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, '--key', key, '--version', version]), - ).resolves.toBe( - `🎭 dry run! This will create 'new-doc' with contents from ./__tests__/${fixturesBaseDir}/new-docs/new-doc.md with the following metadata: ${JSON.stringify( - doc.data, - )}`, - ); - - getMock.done(); - versionMock.done(); - }); - - it('should skip doc if it does not contain any front matter attributes', async () => { - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const filePath = `./__tests__/${fixturesBaseDir}/failure-docs/doc-sans-attributes.md`; - - await expect(run(['--key', key, '--version', version, filePath])).resolves.toBe( - `⏭️ no front matter attributes found for ${filePath}, skipping`, - ); - - versionMock.done(); - }); - - it('should fail if some other error when retrieving page slug', async () => { - const slug = 'new-doc'; - - const errorObject = { - error: 'INTERNAL_ERROR', - message: 'Unknown error (yikes)', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(500, errorObject); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - const filePath = `./__tests__/${fixturesBaseDir}/failure-docs/${slug}.md`; - - const formattedErrorObject = { - ...errorObject, - message: `Error uploading ${chalk.underline(`${filePath}`)}:\n\n${errorObject.message}`, - }; - - await expect(run([filePath, '--key', key, '--version', version])).rejects.toStrictEqual( - new APIv1Error(formattedErrorObject), - ); - - getMock.done(); - versionMock.done(); - }); - }); - - describe('slug metadata', () => { - it('should use provided slug', async () => { - const slug = 'new-doc-slug'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/slug-docs/${slug}.md`))); - - const getMock = getAPIv1Mock() - .get(`/api/v1/docs/${doc.data.slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - const postMock = getAPIv1Mock() - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug: doc.data.slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([`./__tests__/${fixturesBaseDir}/slug-docs/new-doc-slug.md`, '--key', key, '--version', version]), - ).resolves.toBe( - `🌱 successfully created 'marc-actually-wrote-a-test' (ID: 1234) with contents from ./__tests__/${fixturesBaseDir}/slug-docs/new-doc-slug.md`, - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - }); - - describe('existing docs', () => { - let simpleDoc; - - beforeEach(() => { - const fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/simple-doc.md')); - simpleDoc = { - slug: 'simple-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - }); - - it('should fetch doc and merge with what is returned', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const updateMock = getAPIv1MockWithVersionHeader(version) - .put('/api/v1/docs/simple-doc', { - body: simpleDoc.doc.content, - lastUpdatedHash: simpleDoc.hash, - ...simpleDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { - category, - slug: simpleDoc.slug, - body: simpleDoc.doc.content, - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([`./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key, '--version', version]), - ).resolves.toBe( - `✏️ successfully updated 'simple-doc' with contents from ./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - ); - - getMock.done(); - updateMock.done(); - versionMock.done(); - }); - - it('should return doc update info for dry run', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([ - '--dryRun', - `./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - '--key', - key, - '--version', - version, - ]), - ).resolves.toBe( - [ - `🎭 dry run! This will update 'simple-doc' with contents from ./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md with the following metadata: ${JSON.stringify( - simpleDoc.doc.data, - )}`, - ].join('\n'), - ); - - getMock.done(); - versionMock.done(); - }); - - it('should not send requests for docs that have not changed', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([`./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key, '--version', version]), - ).resolves.toBe('`simple-doc` was not updated because there were no changes.'); - - getMock.done(); - versionMock.done(); - }); - - it('should adjust "no changes" message if in dry run', async () => { - const getMock = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: simpleDoc.hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([ - '--dryRun', - `./__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - '--key', - key, - '--version', - version, - ]), - ).resolves.toBe('🎭 dry run! `simple-doc` will not be updated because there were no changes.'); - - getMock.done(); - versionMock.done(); - }); - }); - - describe('command execution in GitHub Actions runner', () => { - beforeEach(() => { - beforeGHAEnv(); - }); - - afterEach(afterGHAEnv); - - it('should sync new doc with correct headers', async () => { - const slug = 'new-doc'; - const id = '1234'; - const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/new-docs/${slug}.md`))); - - const getMock = getAPIv1MockWithVersionHeader(version) - .get(`/api/v1/docs/${slug}`) - .basicAuth({ user: key }) - .reply(404, { - error: 'DOC_NOTFOUND', - message: `The doc with the slug '${slug}' couldn't be found`, - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }); - - 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__/docs/new-docs/new-doc.md', - 'x-readme-version': version, - }) - .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) - .basicAuth({ user: key }) - .reply(201, { slug, _id: id, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([`./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, '--key', key, '--version', version]), - ).resolves.toBe( - `🌱 successfully created 'new-doc' (ID: 1234) with contents from ./__tests__/${fixturesBaseDir}/new-docs/new-doc.md`, - ); - - getMock.done(); - postMock.done(); - versionMock.done(); - }); - - it('should sync existing doc with correct headers', async () => { - const fileContents = fs.readFileSync(path.join(fullFixturesDir, '/existing-docs/simple-doc.md')); - const simpleDoc = { - slug: 'simple-doc', - doc: frontMatter(fileContents), - hash: hashFileContents(fileContents), - }; - - const getMock = getAPIv1MockWithVersionHeader(version) - .get('/api/v1/docs/simple-doc') - .basicAuth({ user: key }) - .reply(200, { category, slug: simpleDoc.slug, lastUpdatedHash: 'anOldHash' }); - - const updateMock = 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__/docs/existing-docs/simple-doc.md', - 'x-readme-version': version, - }) - .put('/api/v1/docs/simple-doc', { - body: simpleDoc.doc.content, - lastUpdatedHash: simpleDoc.hash, - ...simpleDoc.doc.data, - }) - .basicAuth({ user: key }) - .reply(200, { - category, - slug: simpleDoc.slug, - body: simpleDoc.doc.content, - }); - - const versionMock = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect( - run([`__tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, '--key', key, '--version', version]), - ).resolves.toBe( - `✏️ successfully updated 'simple-doc' with contents from __tests__/${fixturesBaseDir}/existing-docs/simple-doc.md`, - ); - - getMock.done(); - updateMock.done(); - versionMock.done(); - }); - }); -}); diff --git a/__tests__/commands/open.test.ts b/__tests__/commands/open.test.ts deleted file mode 100644 index 3ab9829f0..000000000 --- a/__tests__/commands/open.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Version } from '../../src/commands/versions/index.js'; - -import chalk from 'chalk'; -import { describe, afterEach, beforeAll, it, expect } from 'vitest'; - -import pkg from '../../package.json' with { type: 'json' }; -import Command from '../../src/commands/open.js'; -import configStore from '../../src/lib/configstore.js'; -import { getAPIv1Mock } from '../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../helpers/oclif.js'; - -const mockArg = ['--mock']; - -describe('rdme open', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => { - configStore.clear(); - }); - - it('should error if no project provided', () => { - configStore.delete('project'); - - return expect(run(mockArg)).rejects.toStrictEqual(new Error(`Please login using \`${pkg.name} login\`.`)); - }); - - it('should open the project', () => { - configStore.set('project', 'subdomain'); - - const projectUrl = 'https://subdomain.readme.io'; - - return expect(run(mockArg)).resolves.toBe(`Opening ${chalk.green(projectUrl)} in your browser...`); - }); - - describe('open --dash', () => { - it('should open the dash', async () => { - configStore.set('project', 'subdomain'); - configStore.set('apiKey', '12345'); - - const version = '1.0'; - const key = '12345'; - const versionPayload: Version = { - createdAt: '2019-06-17T22:39:56.462Z', - is_deprecated: false, - is_hidden: false, - is_beta: false, - is_stable: true, - codename: '', - version, - }; - - const mockRequest = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [versionPayload, { version: '1.0.1' }]); - - const dashUrl = 'https://dash.readme.com/project/subdomain/v1.0/overview'; - - await expect(run(mockArg.concat('--dash'))).resolves.toBe(`Opening ${chalk.green(dashUrl)} in your browser...`); - mockRequest.done(); - }); - - it('should require user to be logged in', () => { - configStore.set('project', 'subdomain'); - - return expect(run(mockArg.concat('--dash'))).rejects.toStrictEqual( - new Error(`Please login using \`${pkg.name} login\`.`), - ); - }); - }); -}); diff --git a/__tests__/commands/openapi/index.test.ts b/__tests__/commands/openapi/index.test.ts deleted file mode 100644 index d013e4aa7..000000000 --- a/__tests__/commands/openapi/index.test.ts +++ /dev/null @@ -1,1441 +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 config from '../../../src/lib/config.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 = `${config.host}/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__/commands/openapi/upload.test.ts b/__tests__/commands/openapi/upload.test.ts new file mode 100644 index 000000000..7e405999e --- /dev/null +++ b/__tests__/commands/openapi/upload.test.ts @@ -0,0 +1,409 @@ +import nock from 'nock'; +import prompts from 'prompts'; +import slugify from 'slugify'; +import { describe, beforeAll, beforeEach, afterEach, it, expect } from 'vitest'; + +import Command from '../../../src/commands/openapi/upload.js'; +import petstore from '../../__fixtures__/petstore-simple-weird-version.json' with { type: 'json' }; +import { getAPIv2Mock, getAPIv2MockForGHA } from '../../helpers/get-api-mock.js'; +import { runCommand, type OclifOutput } from '../../helpers/oclif.js'; +import { after, before } from '../../helpers/setup-gha-env.js'; + +const key = 'rdme_123'; +const version = '1.0.0'; +const filename = '__tests__/__fixtures__/petstore-simple-weird-version.json'; +const fileUrl = 'https://example.com/openapi.json'; +const slugifiedFilename = slugify.default(filename); + +describe('rdme openapi upload', () => { + let run: (args?: string[]) => OclifOutput; + + beforeAll(() => { + nock.disableNetConnect(); + run = runCommand(Command); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + describe('flag error handling', () => { + it('should throw if an error if both `--version` and `--useSpecVersion` flags are passed', async () => { + const result = await run(['--useSpecVersion', '--version', version, filename, '--key', key]); + expect(result.error.message).toContain('--version=1.0.0 cannot also be provided when using --useSpecVersion'); + }); + }); + + describe('given that the API definition is a local file', () => { + it('should create a new API definition in ReadMe', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.stdout).toContain('was successfully created in ReadMe!'); + + mock.done(); + }); + + it('should update an existing API definition in ReadMe', async () => { + prompts.inject([true]); + + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [{ filename: slugifiedFilename }] }) + .put(`/versions/1.0.0/apis/${slugifiedFilename}`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.stdout).toContain('was successfully updated in ReadMe!'); + + mock.done(); + }); + + it('should handle upload failures', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'fail' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.error.message).toBe( + 'Your API definition upload failed with an unexpected error. Please get in touch with us at support@readme.io.', + ); + + mock.done(); + }); + + describe('and the `--slug` flag is passed', () => { + it('should use the provided slug (no file extension) as the filename', async () => { + const customSlug = 'custom-slug'; + const customSlugWithExtension = `${customSlug}.json`; + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${customSlugWithExtension}"`), + ) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/${customSlugWithExtension}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key, '--slug', customSlug]); + expect(result.stdout).toContain( + `Your API definition (${customSlugWithExtension}) was successfully created in ReadMe!`, + ); + + mock.done(); + }); + + it('should use the provided slug (includes file extension) as the filename', async () => { + const customSlug = 'custom-slug.json'; + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => body.match(`form-data; name="schema"; filename="${customSlug}"`)) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/${customSlug}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key, '--slug', customSlug]); + expect(result.stdout).toContain(`Your API definition (${customSlug}) was successfully created in ReadMe!`); + + mock.done(); + }); + + it('should handle a slug with an invalid file extension', async () => { + const customSlug = 'custom-slug.yikes'; + + const result = await run(['--version', version, filename, '--key', key, '--slug', customSlug]); + expect(result.error.message).toBe( + 'Please provide a valid file extension that matches the extension on the file you provided. Must be `.json`, `.yaml`, or `.yml`.', + ); + }); + + it('should handle a slug with a valid but mismatching file extension', async () => { + const customSlug = 'custom-slug.yml'; + + const result = await run(['--version', version, filename, '--key', key, '--slug', customSlug]); + expect(result.error.message).toBe( + 'Please provide a valid file extension that matches the extension on the file you provided. Must be `.json`, `.yaml`, or `.yml`.', + ); + }); + }); + + describe('and the upload status initially is a pending state', () => { + it('should poll the API until the upload is complete', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'pending' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }) + .get(`/versions/${version}/apis/${slugifiedFilename}`) + .times(9) + .reply(200, { + data: { + upload: { status: 'pending' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }) + .get(`/versions/${version}/apis/${slugifiedFilename}`) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.stdout).toContain('was successfully created in ReadMe!'); + + mock.done(); + }); + + it('should poll the API and handle timeouts', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'pending' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }) + .get(`/versions/${version}/apis/${slugifiedFilename}`) + .times(10) + .reply(200, { + data: { + upload: { status: 'pending' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.error.message).toBe('Sorry, this upload timed out. Please try again later.'); + + mock.done(); + }); + + it('should poll the API once and handle a failure state with a 4xx', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'pending' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }) + .get(`/versions/${version}/apis/${slugifiedFilename}`) + .reply(400); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.error.message).toBe( + 'The ReadMe API responded with an unexpected error. Please try again and if this issue persists, get in touch with us at support@readme.io.', + ); + + mock.done(); + }); + + it('should poll the API once and handle an unexpected state with a 2xx', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${version}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'pending' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }) + .get(`/versions/${version}/apis/${slugifiedFilename}`) + .reply(200, { + data: { + upload: { status: 'something-unexpected' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.error).toStrictEqual( + new Error( + 'Your API definition upload failed with an unexpected error. Please get in touch with us at support@readme.io.', + ), + ); + + mock.done(); + }); + }); + + describe('and the command is being run in a CI environment', () => { + beforeEach(before); + + afterEach(after); + + it('should overwrite an existing API definition without asking for confirmation', async () => { + const mock = getAPIv2MockForGHA({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [{ filename: slugifiedFilename }] }) + .put(`/versions/1.0.0/apis/${slugifiedFilename}`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--version', version, filename, '--key', key]); + expect(result.stdout).toContain('was successfully updated in ReadMe!'); + + mock.done(); + }); + }); + + describe('given that the `--version` flag is not set', () => { + it('should default to the `stable` version', async () => { + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get('/versions/stable/apis') + .reply(200, { data: [] }) + .post('/versions/stable/apis', body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/stable/apis/${slugifiedFilename}`, + }, + }); + + const result = await run([filename, '--key', key]); + expect(result.stdout).toContain('was successfully created in ReadMe!'); + + mock.done(); + }); + + it('should use the version from the spec file if --`useSpecVersion` is passed', async () => { + const altVersion = '1.2.3'; + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${altVersion}/apis`) + .reply(200, { data: [] }) + .post(`/versions/${altVersion}/apis`, body => + body.match(`form-data; name="schema"; filename="${slugifiedFilename}"`), + ) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${altVersion}/apis/${slugifiedFilename}`, + }, + }); + + const result = await run(['--useSpecVersion', filename, '--key', key]); + expect(result.stdout).toContain('was successfully created in ReadMe!'); + + mock.done(); + }); + }); + }); + + describe('given that the API definition is a URL', () => { + it('should create a new API definition in ReadMe', async () => { + const fileMock = nock('https://example.com').get('/openapi.json').reply(200, petstore); + + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, {}) + .post(`/versions/${version}/apis`, body => body.match(`form-data; name="url"\r\n\r\n${fileUrl}`)) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/openapi.json`, + }, + }); + + const result = await run(['--version', version, fileUrl, '--key', key]); + expect(result.stdout).toContain('was successfully created in ReadMe!'); + + fileMock.done(); + mock.done(); + }); + + it('should update an existing API definition in ReadMe', async () => { + prompts.inject([true]); + + const fileMock = nock('https://example.com').get('/openapi.json').reply(200, petstore); + + const mock = getAPIv2Mock({ authorization: `Bearer ${key}` }) + .get(`/versions/${version}/apis`) + .reply(200, { data: [{ filename: 'openapi.json' }] }) + .put('/versions/1.0.0/apis/openapi.json', body => body.match(`form-data; name="url"\r\n\r\n${fileUrl}`)) + .reply(200, { + data: { + upload: { status: 'done' }, + uri: `/versions/${version}/apis/openapi.json`, + }, + }); + + const result = await run(['--version', version, fileUrl, '--key', key]); + expect(result.stdout).toContain('was successfully updated in ReadMe!'); + + fileMock.done(); + mock.done(); + }); + + it('should handle issues fetching from the URL', async () => { + const fileMock = nock('https://example.com').get('/openapi.json').reply(400, {}); + + const result = await run(['--version', version, fileUrl, '--key', key]); + expect(result.error.message).toBe('Unknown file detected.'); + + fileMock.done(); + }); + }); +}); diff --git a/__tests__/commands/versions/create.test.ts b/__tests__/commands/versions/create.test.ts deleted file mode 100644 index 014bad9ff..000000000 --- a/__tests__/commands/versions/create.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import nock from 'nock'; -import prompts from 'prompts'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/versions/create.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme versions create', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => nock.cleanAll()); - - it('should error if no version provided', () => { - return expect(run(['--key', key])).rejects.toThrow('Missing 1 required arg:\nversion'); - }); - - it('should error if invalid version provided', () => { - return expect(run(['--key', key, 'test'])).rejects.toStrictEqual( - new Error('Please specify a semantic version. See `rdme help versions create` for help.'), - ); - }); - - it('should create a specific version', async () => { - prompts.inject([version, false, true, true, false]); - const newVersion = '1.0.1'; - - const mockRequest = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }, { version: '1.1.0' }]) - .post('/api/v1/version', { - version: newVersion, - is_stable: false, - is_beta: true, - from: '1.0.0', - is_hidden: true, - is_deprecated: false, - }) - .basicAuth({ user: key }) - .reply(201, { version: newVersion }); - - await expect(run(['--key', key, newVersion])).resolves.toBe(`Version ${newVersion} created successfully.`); - mockRequest.done(); - }); - - it('should create a specific version with options', async () => { - const newVersion = '1.0.1'; - - const mockRequest = getAPIv1Mock() - .post('/api/v1/version', { - version: newVersion, - codename: 'test', - from: '1.0.0', - is_beta: false, - is_deprecated: false, - is_hidden: false, - is_stable: false, - }) - .basicAuth({ user: key }) - .reply(201, { version: newVersion }); - - await expect( - run([ - '--key', - key, - newVersion, - '--fork', - version, - '--beta', - 'false', - '--deprecated', - 'false', - '--main', - 'false', - '--codename', - 'test', - '--hidden', - 'false', - ]), - ).resolves.toBe(`Version ${newVersion} created successfully.`); - - mockRequest.done(); - }); - - it('should create successfully a main version', async () => { - const newVersion = '1.0.1'; - - const mockRequest = getAPIv1Mock() - .post('/api/v1/version', { - version: newVersion, - from: '1.0.0', - is_beta: false, - is_stable: true, - }) - .basicAuth({ user: key }) - .reply(201, { version: newVersion }); - - await expect( - run([ - '--key', - key, - newVersion, - '--fork', - version, - '--beta', - 'false', - '--main', - 'true', - '--hidden', - 'true', - '--deprecated', - 'true', - ]), - ).resolves.toBe(`Version ${newVersion} created successfully.`); - - mockRequest.done(); - }); - - it('should catch any post request errors', async () => { - const errorResponse = { - error: 'VERSION_EMPTY', - message: 'You need to include an x-readme-version header', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const mockRequest = getAPIv1Mock().post('/api/v1/version').basicAuth({ user: key }).reply(400, errorResponse); - - await expect(run(['--key', key, version, '--fork', '0.0.5'])).rejects.toStrictEqual(new APIv1Error(errorResponse)); - mockRequest.done(); - }); - - describe('bad flag values', () => { - it('should throw if non-boolean `beta` flag is passed', () => { - const newVersion = '1.0.1'; - - return expect(run(['--key', key, newVersion, '--fork', version, '--beta', 'test'])).rejects.toThrow( - 'Expected --beta=test to be one of: true, false', - ); - }); - - it('should throw if non-boolean `deprecated` flag is passed', () => { - const newVersion = '1.0.1'; - - return expect(run(['--key', key, newVersion, '--fork', version, '--deprecated', 'test'])).rejects.toThrow( - 'Expected --deprecated=test to be one of: true, false', - ); - }); - - it('should throw if non-boolean `hidden` flag is passed', () => { - const newVersion = '1.0.1'; - - return expect(run(['--key', key, newVersion, '--fork', version, '--hidden', 'test'])).rejects.toThrow( - 'Expected --hidden=test to be one of: true, false', - ); - }); - - it('should throw if non-boolean `main` flag is passed', () => { - const newVersion = '1.0.1'; - - return expect(run(['--key', key, newVersion, '--fork', version, '--main', 'test'])).rejects.toThrow( - 'Expected --main=test to be one of: true, false', - ); - }); - }); -}); diff --git a/__tests__/commands/versions/delete.test.ts b/__tests__/commands/versions/delete.test.ts deleted file mode 100644 index a82b11efe..000000000 --- a/__tests__/commands/versions/delete.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import nock from 'nock'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/versions/delete.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme versions delete', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => nock.cleanAll()); - - it('should delete a specific version', async () => { - const mockRequest = getAPIv1Mock() - .delete(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { removed: true }) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, version])).resolves.toBe('Version 1.0.0 deleted successfully.'); - mockRequest.done(); - }); - - it('should catch any request errors', async () => { - const errorResponse = { - error: '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 mockRequest = getAPIv1Mock() - .delete(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(404, errorResponse) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); - - await expect(run(['--key', key, version])).rejects.toStrictEqual(new APIv1Error(errorResponse)); - mockRequest.done(); - }); -}); diff --git a/__tests__/commands/versions/index.test.ts b/__tests__/commands/versions/index.test.ts deleted file mode 100644 index 7ba6650d7..000000000 --- a/__tests__/commands/versions/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Version } from '../../../src/commands/versions/index.js'; - -import nock from 'nock'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/versions/index.js'; -import { getAPIv1Mock } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const key = 'API_KEY'; -const version = '1.0.0'; -const version2 = '2.0.0'; - -const versionPayload: Version = { - createdAt: '2019-06-17T22:39:56.462Z', - is_deprecated: false, - is_hidden: false, - is_beta: false, - is_stable: true, - codename: '', - version, -}; - -const version2Payload: Version = { - createdAt: '2019-06-17T22:39:56.462Z', - is_deprecated: false, - is_hidden: false, - is_beta: false, - is_stable: true, - codename: '', - version: version2, -}; - -describe('rdme versions', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => nock.cleanAll()); - - it('should make a request to get a list of existing versions', async () => { - const mockRequest = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [versionPayload, version2Payload]); - - const output = await run(['--key', key]); - expect(output).toStrictEqual(JSON.stringify([versionPayload, version2Payload], null, 2)); - mockRequest.done(); - }); - - it('should get a specific version object if version flag provided', async () => { - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, versionPayload); - - const output = await run(['--key', key, '--version', version]); - expect(output).toStrictEqual(JSON.stringify(versionPayload, null, 2)); - mockRequest.done(); - }); -}); diff --git a/__tests__/commands/versions/update.test.ts b/__tests__/commands/versions/update.test.ts deleted file mode 100644 index 66734d9ab..000000000 --- a/__tests__/commands/versions/update.test.ts +++ /dev/null @@ -1,388 +0,0 @@ -import nock from 'nock'; -import prompts from 'prompts'; -import { describe, beforeAll, afterEach, it, expect } from 'vitest'; - -import Command from '../../../src/commands/versions/update.js'; -import { APIv1Error } from '../../../src/lib/apiError.js'; -import { getAPIv1Mock } from '../../helpers/get-api-mock.js'; -import { runCommandAndReturnResult } from '../../helpers/oclif.js'; - -const key = 'API_KEY'; -const version = '1.0.0'; - -describe('rdme versions update', () => { - let run: (args?: string[]) => Promise; - - beforeAll(() => { - nock.disableNetConnect(); - run = runCommandAndReturnResult(Command); - }); - - afterEach(() => nock.cleanAll()); - - it('should update a specific version object using prompts', async () => { - const versionToChange = '1.1.0'; - prompts.inject([versionToChange, undefined, false, true, false, false]); - - const updatedVersionObject = { - version: versionToChange, - is_stable: false, - is_beta: true, - is_deprecated: false, - is_hidden: false, - }; - - const mockRequest = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }, { version: versionToChange }]) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect(run(['--key', key])).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it('should rename a specific version object using prompts', async () => { - const versionToChange = '1.1.0'; - const renamedVersion = '1.1.0-update'; - prompts.inject([versionToChange, renamedVersion, false, true, false, false]); - - const updatedVersionObject = { - version: renamedVersion, - is_stable: false, - is_beta: true, - is_deprecated: false, - is_hidden: false, - }; - - const mockRequest = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }, { version: versionToChange }]) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect(run(['--key', key])).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it('should use subset of prompts when updating stable version', async () => { - const versionToChange = '1.1.0'; - prompts.inject([versionToChange, undefined, true]); - - const updatedVersionObject = { - version: versionToChange, - is_beta: true, - }; - - const mockRequest = getAPIv1Mock() - .get('/api/v1/version') - .basicAuth({ user: key }) - .reply(200, [{ version }, { version: versionToChange, is_stable: true }]) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange, is_stable: true }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect(run(['--key', key])).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it('should update a specific version object using flags', async () => { - const versionToChange = '1.1.0'; - const renamedVersion = '1.1.0-update'; - - const updatedVersionObject = { - codename: 'updated-test', - version: renamedVersion, - is_beta: true, - is_deprecated: true, - is_hidden: false, - is_stable: false, - }; - - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect( - run([ - '--key', - key, - versionToChange, - '--newVersion', - renamedVersion, - '--deprecated', - 'true', - '--beta', - 'true', - '--main', - 'false', - '--codename', - 'updated-test', - '--hidden', - 'false', - ]), - ).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it("should update a specific version object using flags that contain the string 'false'", async () => { - const versionToChange = '1.1.0'; - const renamedVersion = '1.1.0-update'; - - const updatedVersionObject = { - codename: 'updated-test', - version: renamedVersion, - is_beta: false, - is_deprecated: false, - is_hidden: true, - is_stable: false, - }; - - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect( - run([ - '--key', - key, - versionToChange, - '--newVersion', - renamedVersion, - '--beta', - 'false', - '--deprecated', - 'false', - '--main', - 'false', - '--codename', - 'updated-test', - '--hidden', - 'true', - ]), - ).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it("should update a specific version object using flags that contain the string 'false' and a prompt", async () => { - const versionToChange = '1.1.0'; - const renamedVersion = '1.1.0-update'; - // prompt for beta flag - prompts.inject([false]); - - const updatedVersionObject = { - codename: 'updated-test', - version: renamedVersion, - is_beta: false, - is_hidden: false, - is_stable: false, - }; - - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect( - run([ - '--key', - key, - versionToChange, - '--newVersion', - renamedVersion, - '--main', - 'false', - '--codename', - 'updated-test', - '--hidden', - 'false', - ]), - ).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it('should update a specific version object even if user bypasses prompt for new version name', async () => { - const versionToChange = '1.1.0'; - // simulating user entering nothing for the prompt to enter a new version name - prompts.inject(['']); - - const updatedVersionObject = { - codename: 'updated-test', - is_beta: false, - is_hidden: false, - is_stable: false, - version: versionToChange, - }; - - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect( - run([ - '--key', - key, - versionToChange, - '--beta', - 'false', - '--main', - 'false', - '--codename', - 'updated-test', - '--hidden', - 'false', - ]), - ).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it('should update a version to be the main one', async () => { - const versionToChange = '1.1.0'; - const renamedVersion = '1.1.0-update'; - - const updatedVersionObject = { - version: renamedVersion, - is_beta: false, - is_stable: true, - }; - - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .get(`/api/v1/version/${versionToChange}`) - .basicAuth({ user: key }) - .reply(200, { version: versionToChange }) - .put(`/api/v1/version/${versionToChange}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(201, updatedVersionObject); - - await expect( - run([ - '--key', - key, - versionToChange, - '--newVersion', - renamedVersion, - '--deprecated', - 'true', - '--beta', - 'false', - '--main', - 'true', - '--hidden', - 'true', - ]), - ).resolves.toBe(`Version ${versionToChange} updated successfully.`); - mockRequest.done(); - }); - - it('should catch any put request errors', async () => { - const renamedVersion = '1.0.0-update'; - - const updatedVersionObject = { - version: renamedVersion, - is_beta: true, - is_deprecated: true, - is_hidden: false, - is_stable: false, - }; - - prompts.inject([renamedVersion, false, true, false, true]); - - const errorResponse = { - error: 'VERSION_DUPLICATE', - message: 'The version already exists.', - suggestion: '...a suggestion to resolve the issue...', - help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', - }; - - const mockRequest = getAPIv1Mock() - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }) - .put(`/api/v1/version/${version}`, updatedVersionObject) - .basicAuth({ user: key }) - .reply(400, errorResponse); - - await expect(run(['--key', key, version])).rejects.toStrictEqual(new APIv1Error(errorResponse)); - mockRequest.done(); - }); - - describe('bad flag values', () => { - it('should throw if non-boolean `beta` flag is passed', () => { - const versionToChange = '1.1.0'; - - return expect(run(['--key', key, versionToChange, '--beta', 'hi'])).rejects.toThrow( - 'Expected --beta=hi to be one of: true, false', - ); - }); - - it('should throw if non-boolean `deprecated` flag is passed', () => { - const versionToChange = '1.1.0'; - - return expect(run(['--key', key, versionToChange, '--deprecated', 'hi'])).rejects.toThrow( - 'Expected --deprecated=hi to be one of: true, false', - ); - }); - - it('should throw if non-boolean `hidden` flag is passed', () => { - const versionToChange = '1.1.0'; - - return expect(run(['--key', key, versionToChange, '--hidden', 'hi'])).rejects.toThrow( - 'Expected --hidden=hi to be one of: true, false', - ); - }); - - it('should throw if non-boolean `main` flag is passed', () => { - const versionToChange = '1.1.0'; - - return expect(run(['--key', key, versionToChange, '--main', 'hi'])).rejects.toThrow( - 'Expected --main=hi to be one of: true, false', - ); - }); - }); -}); diff --git a/__tests__/helpers/get-api-mock.ts b/__tests__/helpers/get-api-mock.ts index c8fc83ff9..687ddabe5 100644 --- a/__tests__/helpers/get-api-mock.ts +++ b/__tests__/helpers/get-api-mock.ts @@ -3,12 +3,14 @@ import nock from 'nock'; import config from '../../src/lib/config.js'; import { getUserAgent } from '../../src/lib/readmeAPIFetch.js'; +import { mockVersion } from './oclif.js'; + /** * Nock wrapper for ReadMe API v1 that adds required * `user-agent` request header so it gets properly picked up by nock. */ export function getAPIv1Mock(reqHeaders = {}) { - return nock(config.host, { + return nock(config.host.v1, { reqheaders: { 'User-Agent': getUserAgent(), ...reqHeaders, @@ -16,8 +18,27 @@ export function getAPIv1Mock(reqHeaders = {}) { }); } -export function getAPIv1MockWithVersionHeader(v: string) { - return getAPIv1Mock({ - 'x-readme-version': v, +/** + * Nock wrapper for ReadMe API v2 that adds required + * `user-agent` request header so it gets properly picked up by nock. + */ +export function getAPIv2Mock(reqHeaders: nock.Options['reqheaders'] = {}) { + return nock(config.host.v2, { + reqheaders: { + 'User-Agent': ua => ua.startsWith(`rdme/${mockVersion}`), + 'x-readme-source': 'cli', + ...reqHeaders, + }, + }); +} + +/** + * Variant of `getAPIv2Mock` for mocking a GitHub Actions environment. + */ +export function getAPIv2MockForGHA(reqHeaders: nock.Options['reqheaders'] = {}) { + return getAPIv2Mock({ + 'User-Agent': ua => ua.startsWith(`rdme-github/${mockVersion}`), + 'x-readme-source': 'cli-gh', + ...reqHeaders, }); } diff --git a/__tests__/helpers/oclif.ts b/__tests__/helpers/oclif.ts index a6ec356c4..9462dbed5 100644 --- a/__tests__/helpers/oclif.ts +++ b/__tests__/helpers/oclif.ts @@ -5,8 +5,12 @@ import path from 'node:path'; import { Config } from '@oclif/core'; import { captureOutput, runCommand as oclifRunCommand } from '@oclif/test'; +export type OclifOutput = ReturnType>; + const testNodeEnv = process.env.NODE_ENV; +export const mockVersion = '7.0.0'; + /** * Used for setting up the oclif configuration for simulating commands in tests. * This is a really barebones approach so we can continue using vitest + nock @@ -21,7 +25,7 @@ export function setupOclifConfig() { return Config.load({ root, - version: '7.0.0', + version: mockVersion, }); } @@ -32,7 +36,7 @@ export function setupOclifConfig() { * * @example runCommand(LoginCommand)(['--email', 'owlbert@example.com', '--password', 'password']) */ -function runCommand(Command: T) { +export function runCommand(Command: T) { return async function runCommandAgainstArgs(args?: string[]) { const oclifConfig = await setupOclifConfig(); // @ts-expect-error this is the pattern recommended by the @oclif/test docs. diff --git a/__tests__/lib/__snapshots__/createGHA.test.ts.snap b/__tests__/lib/__snapshots__/createGHA.test.ts.snap index fce4a5120..1937da436 100644 --- a/__tests__/lib/__snapshots__/createGHA.test.ts.snap +++ b/__tests__/lib/__snapshots__/createGHA.test.ts.snap @@ -164,416 +164,6 @@ jobs: " `; -exports[`#createGHA > command inputs > 'custompages' ' (single)' > should run GHA creation workflow and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-custompages.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'custompages' ' (single)' > should run GHA creation workflow and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`some-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - some-branch - -jobs: - rdme-custompages: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`custompages\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: custompages ./custompages/rdme.md --key=\${{ secrets.README_API_KEY }} -" -`; - -exports[`#createGHA > command inputs > 'custompages' ' (single)' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-custompages-with-github-flag.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'custompages' ' (single)' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`another-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - another-branch - -jobs: - rdme-custompages: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`custompages\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: custompages ./custompages/rdme.md --key=\${{ secrets.README_API_KEY }} -" -`; - -exports[`#createGHA > command inputs > 'custompages' '' > should run GHA creation workflow and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-custompages.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'custompages' '' > should run GHA creation workflow and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`some-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - some-branch - -jobs: - rdme-custompages: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`custompages\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: custompages ./custompages --key=\${{ secrets.README_API_KEY }} -" -`; - -exports[`#createGHA > command inputs > 'custompages' '' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-custompages-with-github-flag.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'custompages' '' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`another-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - another-branch - -jobs: - rdme-custompages: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`custompages\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: custompages ./custompages --key=\${{ secrets.README_API_KEY }} -" -`; - -exports[`#createGHA > command inputs > 'docs' ' (single)' > should run GHA creation workflow and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-docs.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'docs' ' (single)' > should run GHA creation workflow and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`some-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - some-branch - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./docs/rdme.md --key=\${{ secrets.README_API_KEY }} --version=1.0.0 -" -`; - -exports[`#createGHA > command inputs > 'docs' ' (single)' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-docs-with-github-flag.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'docs' ' (single)' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`another-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - another-branch - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./docs/rdme.md --key=\${{ secrets.README_API_KEY }} --version=1.0.0 -" -`; - -exports[`#createGHA > command inputs > 'docs' '' > should run GHA creation workflow and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-docs.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'docs' '' > should run GHA creation workflow and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`some-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - some-branch - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./docs --key=\${{ secrets.README_API_KEY }} --version=1.0.0 -" -`; - -exports[`#createGHA > command inputs > 'docs' '' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-docs-with-github-flag.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'docs' '' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`another-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - another-branch - -jobs: - rdme-docs: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`docs\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: docs ./docs --key=\${{ secrets.README_API_KEY }} --version=1.0.0 -" -`; - -exports[`#createGHA > command inputs > 'openapi' '' > should run GHA creation workflow and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-openapi.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'openapi' '' > should run GHA creation workflow and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`some-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - some-branch - -jobs: - rdme-openapi: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`openapi\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: openapi petstore.json --key=\${{ secrets.README_API_KEY }} --id=spec_id -" -`; - -exports[`#createGHA > command inputs > 'openapi' '' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 1`] = ` -" -Your GitHub Actions workflow file has been created! ✨ - -Almost done! Just a couple more steps: -1. Push your newly created file (.github/workflows/rdme-openapi-with-github-flag.yml) to GitHub πŸš€ -2. Create a GitHub secret called README_API_KEY and populate the value with your ReadMe API key (β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’I_KEY) πŸ”‘ - -πŸ” Check out GitHub's docs for more info on creating encrypted secrets (https://docs.github.com/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) - -πŸ¦‰ If you have any more questions, feel free to drop us a line! support@readme.io -" -`; - -exports[`#createGHA > command inputs > 'openapi' '' > should run GHA creation workflow with \`--github\` flag and messy file name and generate valid workflow file 2`] = ` -"# This GitHub Actions workflow was auto-generated by the \`rdme\` cli on 2022-01-01T00:00:00.000Z -# You can view our full documentation here: https://docs.readme.com/docs/rdme -name: ReadMe GitHub Action πŸ¦‰ - -on: - push: - branches: - # This workflow will run every time you push code to the following branch: \`another-branch\` - # Check out GitHub's docs for more info on configuring this: - # https://docs.github.com/actions/using-workflows/events-that-trigger-workflows - - another-branch - -jobs: - rdme-openapi: - runs-on: ubuntu-latest - steps: - - name: Check out repo πŸ“š - uses: actions/checkout@v4 - - - name: Run \`openapi\` command πŸš€ - uses: readmeio/rdme@v7 - with: - rdme: openapi petstore.json --key=\${{ secrets.README_API_KEY }} --id=spec_id -" -`; - exports[`#createGHA > command inputs > 'openapi:validate' '' > should run GHA creation workflow and generate valid workflow file 1`] = ` " Your GitHub Actions workflow file has been created! ✨ diff --git a/__tests__/lib/createGHA.test.ts b/__tests__/lib/createGHA.test.ts index 943bb90f9..2f78f0954 100644 --- a/__tests__/lib/createGHA.test.ts +++ b/__tests__/lib/createGHA.test.ts @@ -58,14 +58,6 @@ describe('#createGHA', () => { // `openapi:validate` is the ID we define in src/index.ts for backwards compatibility, // hence we're using this command ID here { cmd: 'openapi:validate', opts: { spec: 'petstore.json' }, label: '' }, - { cmd: 'openapi', opts: { key, spec: 'petstore.json', id: 'spec_id' }, label: '' }, - { cmd: 'docs', opts: { key, path: './docs', version: '1.0.0' }, label: '' }, - { - cmd: 'docs', - - label: ' (single)', - opts: { key, path: './docs/rdme.md', version: '1.0.0' }, - }, { cmd: 'changelogs', opts: { key, path: './changelogs' }, label: '' }, { cmd: 'changelogs', @@ -73,12 +65,6 @@ describe('#createGHA', () => { label: ' (single)', opts: { key, path: './changelogs/rdme.md' }, }, - { cmd: 'custompages', opts: { key, path: './custompages' }, label: '' }, - { - cmd: 'custompages', - label: ' (single)', - opts: { key, path: './custompages/rdme.md' }, - }, ])('$cmd $label', ({ cmd, opts }) => { let CurrentCommand: Command.Class; diff --git a/__tests__/lib/prompts.test.ts b/__tests__/lib/prompts.test.ts deleted file mode 100644 index d1dba0a90..000000000 --- a/__tests__/lib/prompts.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import prompts from 'prompts'; -import { describe, it, expect } from 'vitest'; - -import * as promptHandler from '../../src/lib/prompts.js'; -import promptTerminal from '../../src/lib/promptWrapper.js'; - -const versionlist = [ - { - version: '1', - is_stable: true, - }, - { - version: '2', - is_stable: false, - }, -]; - -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]); - - const answer = await promptTerminal(promptHandler.versionPrompt(versionlist)); - expect(answer).toStrictEqual({ from: '1', is_stable: true, is_beta: true }); - }); - - it('should skip fork prompt if value passed (updating version)', async () => { - prompts.inject(['1.2.1', false, true, true, false]); - - const answer = await promptTerminal(promptHandler.versionPrompt(versionlist, { is_stable: false })); - expect(answer).toStrictEqual({ - newVersion: '1.2.1', - is_stable: false, - is_beta: true, - is_hidden: true, - is_deprecated: false, - }); - }); - }); -}); diff --git a/documentation/commands/categories.md b/documentation/commands/categories.md deleted file mode 100644 index 8779ff5ab..000000000 --- a/documentation/commands/categories.md +++ /dev/null @@ -1,84 +0,0 @@ -`rdme categories` -================= - -List or create categories in your ReadMe developer hub. - -* [`rdme categories`](#rdme-categories) -* [`rdme categories create TITLE`](#rdme-categories-create-title) - -## `rdme categories` - -Get all categories in your ReadMe project. - -``` -USAGE - $ rdme categories --key [--version ] - -FLAGS - --key= (required) ReadMe project API key - --version= ReadMe project version - -DESCRIPTION - Get all categories in your ReadMe project. - -EXAMPLES - Get all categories associated to your project version: - - $ rdme categories --version={project-version} - -FLAG DESCRIPTIONS - --key= ReadMe project API key - - An API key for your ReadMe project. Note that API authentication is required despite being omitted from the example - usage. See our docs for more information: https://github.com/readmeio/rdme/tree/v9#authentication - - --version= ReadMe project version - - If running command in a CI environment and this option is not passed, the main project version will be used. See our - docs for more information: https://docs.readme.com/main/docs/versions -``` - -## `rdme categories create TITLE` - -Create a category with the specified title and guide in your ReadMe project. - -``` -USAGE - $ rdme categories create TITLE --categoryType guide|reference --key [--preventDuplicates] [--version ] - -ARGUMENTS - TITLE Title of the category - -FLAGS - --categoryType=