diff --git a/__tests__/cmds/categories/create.test.ts b/__tests__/cmds/categories/create.test.ts index 965d783ff..99a35450a 100644 --- a/__tests__/cmds/categories/create.test.ts +++ b/__tests__/cmds/categories/create.test.ts @@ -1,4 +1,5 @@ import nock from 'nock'; +import prompts from 'prompts'; import CategoriesCreateCommand from '../../../src/cmds/categories/create'; import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock'; @@ -13,10 +14,19 @@ describe('rdme categories:create', () => { afterEach(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(categoriesCreate.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(categoriesCreate.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(categoriesCreate.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no title provided', () => { diff --git a/__tests__/cmds/categories/index.test.ts b/__tests__/cmds/categories/index.test.ts index d19a195a8..0c9bf8956 100644 --- a/__tests__/cmds/categories/index.test.ts +++ b/__tests__/cmds/categories/index.test.ts @@ -1,4 +1,5 @@ import nock from 'nock'; +import prompts from 'prompts'; import CategoriesCommand from '../../../src/cmds/categories'; import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock'; @@ -13,10 +14,19 @@ describe('rdme categories', () => { afterEach(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(categories.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(categories.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(categories.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should return all categories for a single page', async () => { diff --git a/__tests__/cmds/changelogs/index.test.ts b/__tests__/cmds/changelogs/index.test.ts index a167c621e..1640db25c 100644 --- a/__tests__/cmds/changelogs/index.test.ts +++ b/__tests__/cmds/changelogs/index.test.ts @@ -4,6 +4,7 @@ import path from 'path'; import chalk from 'chalk'; import frontMatter from 'gray-matter'; import nock from 'nock'; +import prompts from 'prompts'; import ChangelogsCommand from '../../../src/cmds/changelogs'; import APIError from '../../../src/lib/apiError'; @@ -21,10 +22,19 @@ describe('rdme changelogs', () => { afterAll(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(changelogs.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(changelogs.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(changelogs.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no folder provided', () => { diff --git a/__tests__/cmds/changelogs/single.test.ts b/__tests__/cmds/changelogs/single.test.ts index b808bdade..d20541e80 100644 --- a/__tests__/cmds/changelogs/single.test.ts +++ b/__tests__/cmds/changelogs/single.test.ts @@ -4,6 +4,7 @@ import path from 'path'; import chalk from 'chalk'; import frontMatter from 'gray-matter'; import nock from 'nock'; +import prompts from 'prompts'; import SingleChangelogCommand from '../../../src/cmds/changelogs/single'; import APIError from '../../../src/lib/apiError'; @@ -21,10 +22,19 @@ describe('rdme changelogs:single', () => { afterAll(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(changelogsSingle.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(changelogsSingle.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(changelogsSingle.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no file path provided', () => { diff --git a/__tests__/cmds/custompages/index.test.ts b/__tests__/cmds/custompages/index.test.ts index 375bd7b26..c1541e7c8 100644 --- a/__tests__/cmds/custompages/index.test.ts +++ b/__tests__/cmds/custompages/index.test.ts @@ -4,6 +4,7 @@ import path from 'path'; import chalk from 'chalk'; import frontMatter from 'gray-matter'; import nock from 'nock'; +import prompts from 'prompts'; import CustomPagesCommand from '../../../src/cmds/custompages'; import APIError from '../../../src/lib/apiError'; @@ -21,10 +22,19 @@ describe('rdme custompages', () => { afterAll(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(custompages.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(custompages.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(custompages.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no folder provided', () => { diff --git a/__tests__/cmds/custompages/single.test.ts b/__tests__/cmds/custompages/single.test.ts index b7c880c47..27dbb55df 100644 --- a/__tests__/cmds/custompages/single.test.ts +++ b/__tests__/cmds/custompages/single.test.ts @@ -4,6 +4,7 @@ import path from 'path'; import chalk from 'chalk'; import frontMatter from 'gray-matter'; import nock from 'nock'; +import prompts from 'prompts'; import SingleCustomPageCommand from '../../../src/cmds/custompages/single'; import APIError from '../../../src/lib/apiError'; @@ -21,10 +22,19 @@ describe('rdme custompages:single', () => { afterAll(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(customPagesSingle.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(customPagesSingle.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(customPagesSingle.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no file path provided', () => { diff --git a/__tests__/cmds/docs/edit.test.ts b/__tests__/cmds/docs/edit.test.ts index bd23401cb..a67a04670 100644 --- a/__tests__/cmds/docs/edit.test.ts +++ b/__tests__/cmds/docs/edit.test.ts @@ -1,5 +1,8 @@ import fs from 'fs'; +import nock from 'nock'; +import prompts from 'prompts'; + import DocsEditCommand from '../../../src/cmds/docs/edit'; import APIError from '../../../src/lib/apiError'; import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock'; @@ -11,10 +14,21 @@ const version = '1.0.0'; const category = 'CATEGORY_ID'; describe('rdme docs:edit', () => { - it('should error if no api key provided', () => { - return expect(docsEdit.run({})).rejects.toStrictEqual( - new Error('No project API key provided. Please use `--key`.') - ); + beforeAll(() => nock.disableNetConnect()); + + afterAll(() => nock.cleanAll()); + + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(docsEdit.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(docsEdit.run({})).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); + delete process.env.TEST_CI; }); it('should error if no slug provided', () => { diff --git a/__tests__/cmds/docs/index.test.ts b/__tests__/cmds/docs/index.test.ts index 19039aac1..d92d26aff 100644 --- a/__tests__/cmds/docs/index.test.ts +++ b/__tests__/cmds/docs/index.test.ts @@ -31,8 +31,17 @@ describe('rdme docs', () => { afterAll(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(docs.run({})).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(docs.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(docs.run({})).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); + delete process.env.TEST_CI; }); it('should error if no folder provided', () => { diff --git a/__tests__/cmds/docs/single.test.ts b/__tests__/cmds/docs/single.test.ts index 8b3ae719c..4e6878662 100644 --- a/__tests__/cmds/docs/single.test.ts +++ b/__tests__/cmds/docs/single.test.ts @@ -4,6 +4,7 @@ import path from 'path'; import chalk from 'chalk'; import frontMatter from 'gray-matter'; import nock from 'nock'; +import prompts from 'prompts'; import DocsSingleCommand from '../../../src/cmds/docs/single'; import APIError from '../../../src/lib/apiError'; @@ -24,10 +25,19 @@ describe('rdme docs:single', () => { afterAll(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(docsSingle.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(docsSingle.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(docsSingle.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no file path provided', () => { diff --git a/__tests__/cmds/login.test.ts b/__tests__/cmds/login.test.ts index 7774b9baa..36833fc03 100644 --- a/__tests__/cmds/login.test.ts +++ b/__tests__/cmds/login.test.ts @@ -36,7 +36,7 @@ describe('rdme login', () => { it('should post to /login on the API', async () => { prompts.inject([email, password, project]); - const mock = getAPIMock().post('/api/v1/login').reply(200, { apiKey }); + const mock = getAPIMock().post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); await expect(cmd.run({})).resolves.toBe('Successfully logged in as user@example.com to the subdomain project.'); @@ -50,7 +50,7 @@ describe('rdme login', () => { it('should post to /login on the API if passing in project via opt', async () => { prompts.inject([email, password]); - const mock = getAPIMock().post('/api/v1/login').reply(200, { apiKey }); + const mock = getAPIMock().post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); await expect(cmd.run({ project })).resolves.toBe( 'Successfully logged in as user@example.com to the subdomain project.' diff --git a/__tests__/cmds/openapi/index.test.ts b/__tests__/cmds/openapi/index.test.ts index 91b8a29bd..22915e9e8 100644 --- a/__tests__/cmds/openapi/index.test.ts +++ b/__tests__/cmds/openapi/index.test.ts @@ -839,10 +839,17 @@ describe('rdme openapi', () => { }); describe('error handling', () => { - it('should error if no api key provided', () => { - return expect( - openapi.run({ spec: require.resolve('@readme/oas-examples/3.0/json/petstore.json') }) - ).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); + it('should prompt for login if no API key provided', () => { + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + return expect(openapi.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(openapi.run({})).rejects.toStrictEqual( + new Error('No project API key provided. Please use `--key`.') + ); + delete process.env.TEST_CI; }); it('should error if `--create` and `--update` flags are passed simultaneously', () => { diff --git a/__tests__/cmds/versions/create.test.ts b/__tests__/cmds/versions/create.test.ts index 9f8d1af33..765c5336e 100644 --- a/__tests__/cmds/versions/create.test.ts +++ b/__tests__/cmds/versions/create.test.ts @@ -15,10 +15,19 @@ describe('rdme versions:create', () => { afterEach(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(createVersion.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(createVersion.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(createVersion.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should error if no version provided', () => { diff --git a/__tests__/cmds/versions/delete.test.ts b/__tests__/cmds/versions/delete.test.ts index 6fc17e0dc..d27443335 100644 --- a/__tests__/cmds/versions/delete.test.ts +++ b/__tests__/cmds/versions/delete.test.ts @@ -1,4 +1,5 @@ import nock from 'nock'; +import prompts from 'prompts'; import DeleteVersionCommand from '../../../src/cmds/versions/delete'; import APIError from '../../../src/lib/apiError'; @@ -14,10 +15,19 @@ describe('rdme versions:delete', () => { afterEach(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(deleteVersion.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(deleteVersion.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(deleteVersion.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should delete a specific version', async () => { diff --git a/__tests__/cmds/versions/index.test.ts b/__tests__/cmds/versions/index.test.ts index 47bf9e2b5..3c64ea5ff 100644 --- a/__tests__/cmds/versions/index.test.ts +++ b/__tests__/cmds/versions/index.test.ts @@ -1,6 +1,7 @@ import type { Version } from '../../../src/cmds/versions'; import nock from 'nock'; +import prompts from 'prompts'; import VersionsCommand from '../../../src/cmds/versions'; import getAPIMock from '../../helpers/get-api-mock'; @@ -36,10 +37,17 @@ describe('rdme versions', () => { afterEach(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(versions.run({})).rejects.toStrictEqual( - new Error('No project API key provided. Please use `--key`.') - ); + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(versions.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(versions.run({})).rejects.toStrictEqual(new Error('No project API key provided. Please use `--key`.')); + delete process.env.TEST_CI; }); it('should make a request to get a list of existing versions', async () => { diff --git a/__tests__/cmds/versions/update.test.ts b/__tests__/cmds/versions/update.test.ts index 2d55fa136..a45c7d0b7 100644 --- a/__tests__/cmds/versions/update.test.ts +++ b/__tests__/cmds/versions/update.test.ts @@ -15,10 +15,19 @@ describe('rdme versions:update', () => { afterEach(() => nock.cleanAll()); - it('should error if no api key provided', () => { - return expect(updateVersion.run({})).rejects.toStrictEqual( + it('should prompt for login if no API key provided', async () => { + const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + prompts.inject(['this-is-not-an-email', 'password', 'subdomain']); + await expect(updateVersion.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.')); + consoleInfoSpy.mockRestore(); + }); + + it('should error in CI if no API key provided', async () => { + process.env.TEST_CI = 'true'; + await expect(updateVersion.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') ); + delete process.env.TEST_CI; }); it('should update a specific version object', async () => { diff --git a/src/cmds/categories/create.ts b/src/cmds/categories/create.ts index e18c78844..2019153ca 100644 --- a/src/cmds/categories/create.ts +++ b/src/cmds/categories/create.ts @@ -54,7 +54,7 @@ export default class CategoriesCreateCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { categoryType, title, key, version, preventDuplicates } = opts; diff --git a/src/cmds/categories/index.ts b/src/cmds/categories/index.ts index 050c7c440..933f9e444 100644 --- a/src/cmds/categories/index.ts +++ b/src/cmds/categories/index.ts @@ -18,7 +18,7 @@ export default class CategoriesCommand extends Command { } async run(opts: CommandOptions<{}>) { - super.run(opts); + await super.run(opts); const { key, version } = opts; diff --git a/src/cmds/changelogs/index.ts b/src/cmds/changelogs/index.ts index 0b37e93dd..f63a3e377 100644 --- a/src/cmds/changelogs/index.ts +++ b/src/cmds/changelogs/index.ts @@ -42,7 +42,7 @@ export default class ChangelogsCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, folder, key } = opts; diff --git a/src/cmds/changelogs/single.ts b/src/cmds/changelogs/single.ts index 5f9e7fd16..ebccc971c 100644 --- a/src/cmds/changelogs/single.ts +++ b/src/cmds/changelogs/single.ts @@ -41,7 +41,7 @@ export default class SingleChangelogCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, filePath, key } = opts; diff --git a/src/cmds/custompages/index.ts b/src/cmds/custompages/index.ts index 3dd1bafe0..1ebebe172 100644 --- a/src/cmds/custompages/index.ts +++ b/src/cmds/custompages/index.ts @@ -42,7 +42,7 @@ export default class CustomPagesCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, folder, key } = opts; diff --git a/src/cmds/custompages/single.ts b/src/cmds/custompages/single.ts index d2c5f7f5b..0922baabb 100644 --- a/src/cmds/custompages/single.ts +++ b/src/cmds/custompages/single.ts @@ -40,7 +40,7 @@ export default class SingleCustomPageCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, filePath, key } = opts; diff --git a/src/cmds/docs/edit.ts b/src/cmds/docs/edit.ts index 859cfc1f8..24b66c1df 100644 --- a/src/cmds/docs/edit.ts +++ b/src/cmds/docs/edit.ts @@ -45,7 +45,7 @@ export default class EditDocsCommand extends Command { } async run(opts: CommandOptions): Promise { - super.run(opts); + await super.run(opts); const { slug, key, version } = opts; diff --git a/src/cmds/docs/index.ts b/src/cmds/docs/index.ts index 9fe7cc9e0..9198b21d0 100644 --- a/src/cmds/docs/index.ts +++ b/src/cmds/docs/index.ts @@ -59,7 +59,7 @@ export default class DocsCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, folder, key, version, cleanup } = opts; diff --git a/src/cmds/docs/single.ts b/src/cmds/docs/single.ts index da4ad3679..a13cd1aa2 100644 --- a/src/cmds/docs/single.ts +++ b/src/cmds/docs/single.ts @@ -42,7 +42,7 @@ export default class SingleDocCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, filePath, key, version } = opts; diff --git a/src/cmds/login.ts b/src/cmds/login.ts index 883871f1c..43c8282cd 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -1,35 +1,14 @@ import type { CommandOptions } from '../lib/baseCommand'; -import chalk from 'chalk'; -import config from 'config'; import prompts from 'prompts'; -import isEmail from 'validator/lib/isEmail'; import Command, { CommandCategories } from '../lib/baseCommand'; -import configStore from '../lib/configstore'; -import fetch, { handleRes } from '../lib/fetch'; -import { debug } from '../lib/logger'; -import promptTerminal from '../lib/promptWrapper'; +import loginFlow from '../lib/loginFlow'; export type Options = { project?: string; }; -type LoginBody = { - email?: string; - password?: string; - project?: string; - token?: string; -}; - -function loginFetch(body: LoginBody) { - return fetch(`${config.get('host')}/api/v1/login`, { - method: 'post', - headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); -} - export default class LoginCommand extends Command { constructor() { super(); @@ -50,63 +29,10 @@ export default class LoginCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); prompts.override(opts); - const { email, password, project } = await promptTerminal([ - { - type: 'text', - name: 'email', - message: 'What is your email address?', - initial: configStore.get('email'), - validate(val) { - return isEmail(val) ? true : 'Please provide a valid email address.'; - }, - }, - { - type: 'invisible', - name: 'password', - message: 'What is your password?', - }, - { - type: 'text', - name: 'project', - message: 'What project are you logging into?', - initial: configStore.get('project'), - }, - ]); - - if (!project) { - return Promise.reject(new Error('No project subdomain provided. Please use `--project`.')); - } - - if (!isEmail(email)) { - return Promise.reject(new Error('You must provide a valid email address.')); - } - - return loginFetch({ email, password, project }) - .then(handleRes) - .catch(async err => { - // if the user's login requires 2FA, let's prompt them for the token! - if (err.code === 'LOGIN_TWOFACTOR') { - debug('2FA error response, prompting for 2FA code'); - const { token } = await promptTerminal({ - type: 'text', - name: 'token', - message: 'What is your 2FA token?', - }); - - return loginFetch({ email, password, project, token }).then(handleRes); - } - throw err; - }) - .then(res => { - configStore.set('apiKey', res.apiKey); - configStore.set('email', email); - configStore.set('project', project); - - return `Successfully logged in as ${chalk.green(email)} to the ${chalk.blue(project)} project.`; - }); + return loginFlow(); } } diff --git a/src/cmds/logout.ts b/src/cmds/logout.ts index ebe0448f5..3ff0709e8 100644 --- a/src/cmds/logout.ts +++ b/src/cmds/logout.ts @@ -19,7 +19,7 @@ export default class LogoutCommand extends Command { } async run(opts: CommandOptions<{}>) { - super.run(opts); + await super.run(opts); if (configStore.has('email') && configStore.has('project')) { configStore.clear(); diff --git a/src/cmds/oas.ts b/src/cmds/oas.ts index 0c623cc6b..4d676b649 100644 --- a/src/cmds/oas.ts +++ b/src/cmds/oas.ts @@ -18,7 +18,7 @@ export default class OASCommand extends Command { } async run(opts: CommandOptions<{}>) { - super.run(opts); + await super.run(opts); const message = [ 'This `oas` integration is now inactive.', diff --git a/src/cmds/open.ts b/src/cmds/open.ts index 4063b81cc..67b6ea984 100644 --- a/src/cmds/open.ts +++ b/src/cmds/open.ts @@ -25,7 +25,7 @@ export default class OpenCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const project = configStore.get('project'); Command.debug(`project: ${project}`); diff --git a/src/cmds/openapi/index.ts b/src/cmds/openapi/index.ts index f9ac8d23d..f6b9ff9ff 100644 --- a/src/cmds/openapi/index.ts +++ b/src/cmds/openapi/index.ts @@ -91,7 +91,7 @@ export default class OpenAPICommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { dryRun, key, id, spec, create, raw, useSpecVersion, version, workingDirectory, update } = opts; diff --git a/src/cmds/openapi/reduce.ts b/src/cmds/openapi/reduce.ts index bce67d538..601f50788 100644 --- a/src/cmds/openapi/reduce.ts +++ b/src/cmds/openapi/reduce.ts @@ -45,7 +45,7 @@ export default class OpenAPIReduceCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { spec, workingDirectory } = opts; diff --git a/src/cmds/openapi/validate.ts b/src/cmds/openapi/validate.ts index 6cc8ad90c..8324c420a 100644 --- a/src/cmds/openapi/validate.ts +++ b/src/cmds/openapi/validate.ts @@ -38,7 +38,7 @@ export default class OpenAPIValidateCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { spec, workingDirectory } = opts; diff --git a/src/cmds/versions/create.ts b/src/cmds/versions/create.ts index f8d9e7f04..068443927 100644 --- a/src/cmds/versions/create.ts +++ b/src/cmds/versions/create.ts @@ -47,7 +47,7 @@ export default class CreateVersionCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); let versionList; const { key, version, fork, codename, main, beta, isPublic } = opts; diff --git a/src/cmds/versions/delete.ts b/src/cmds/versions/delete.ts index 3025ff9b1..fc1dc799d 100644 --- a/src/cmds/versions/delete.ts +++ b/src/cmds/versions/delete.ts @@ -28,7 +28,7 @@ export default class DeleteVersionCommand extends Command { } async run(opts: CommandOptions<{}>) { - super.run(opts); + await super.run(opts); const { key, version } = opts; diff --git a/src/cmds/versions/index.ts b/src/cmds/versions/index.ts index 05bf7b2cb..cd22a51fd 100644 --- a/src/cmds/versions/index.ts +++ b/src/cmds/versions/index.ts @@ -37,7 +37,7 @@ export default class VersionsCommand extends Command { } async run(opts: CommandOptions<{}>) { - super.run(opts); + await super.run(opts); const { key, version } = opts; diff --git a/src/cmds/versions/update.ts b/src/cmds/versions/update.ts index 80f84199c..4c0b808f8 100644 --- a/src/cmds/versions/update.ts +++ b/src/cmds/versions/update.ts @@ -46,7 +46,7 @@ export default class UpdateVersionCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts); + await super.run(opts); const { key, version, newVersion, codename, main, beta, isPublic, deprecated } = opts; diff --git a/src/cmds/whoami.ts b/src/cmds/whoami.ts index fec952000..d1d075fbd 100644 --- a/src/cmds/whoami.ts +++ b/src/cmds/whoami.ts @@ -20,7 +20,7 @@ export default class WhoAmICommand extends Command { } async run(opts: CommandOptions<{}>) { - super.run(opts); + await super.run(opts); if (!configStore.has('email') || !configStore.has('project')) { return Promise.reject(new Error(`Please login using \`${config.get('cli')} login\`.`)); diff --git a/src/lib/baseCommand.ts b/src/lib/baseCommand.ts index f475ef3ef..1152ea63b 100644 --- a/src/lib/baseCommand.ts +++ b/src/lib/baseCommand.ts @@ -3,8 +3,10 @@ import type commands from '../cmds'; import type { CommandLineOptions } from 'command-line-args'; import type { OptionDefinition } from 'command-line-usage'; +import configstore from './configstore'; import isCI from './isCI'; import { debug, info, warn } from './logger'; +import loginFlow from './loginFlow'; export type CommandOptions = T & { key?: string; @@ -86,13 +88,20 @@ export default class Command { */ args: OptionDefinition[]; - run(opts: CommandOptions<{}>): Promise { + async run(opts: CommandOptions<{}>): Promise { Command.debug(`command: ${this.command}`); Command.debug(`opts: ${JSON.stringify(opts)}`); if (this.args.some(arg => arg.name === 'key')) { if (!opts.key) { - throw new Error('No project API key provided. Please use `--key`.'); + if (isCI()) { + throw new Error('No project API key provided. Please use `--key`.'); + } + info("Looks like you're missing a ReadMe API key, let's fix that! šŸ¦‰", false); + const result = await loginFlow(); + info(result, false); + // eslint-disable-next-line no-param-reassign + opts.key = configstore.get('apiKey'); } } diff --git a/src/lib/createGHA/index.ts b/src/lib/createGHA/index.ts index c929137d7..ef922081a 100644 --- a/src/lib/createGHA/index.ts +++ b/src/lib/createGHA/index.ts @@ -16,7 +16,7 @@ import { checkFilePath, cleanFileName } from '../checkFile'; import configstore from '../configstore'; import { getPkgVersion } from '../getPkgVersion'; import isCI from '../isCI'; -import { debug } from '../logger'; +import { debug, info } from '../logger'; import promptTerminal from '../promptWrapper'; import yamlBase from './baseFile'; @@ -192,24 +192,12 @@ export default async function createGHA( } } - /** - * The reason we're using console.info() in these lines as opposed to - * our logger is because that logger has some formatting limitations - * and this function doesn't ever run in a GitHub Actions environment. - * By using `info` as opposed to `log`, we also can mock it in our tests - * while also freely using `log` when debugging our code. - * - * @see {@link https://github.com/readmeio/rdme/blob/main/CONTRIBUTING.md#usage-of-console} - */ - // eslint-disable-next-line no-console - if (msg) console.info(msg); + if (msg) info(msg, false); if (opts.github) { - // eslint-disable-next-line no-console - console.info(chalk.bold("\nšŸš€ Let's get you set up with GitHub Actions! šŸš€\n")); + info(chalk.bold("\nšŸš€ Let's get you set up with GitHub Actions! šŸš€\n", false)); } else { - // eslint-disable-next-line no-console - console.info( + info( [ '', chalk.bold("šŸ™ Looks like you're running this command in a GitHub Repository! šŸ™"), @@ -220,7 +208,8 @@ export default async function createGHA( '', `āœØ This means it will run ${chalk.italic('automagically')} with every push to a branch of your choice!`, '', - ].join('\n') + ].join('\n'), + false ); } diff --git a/src/lib/logger.ts b/src/lib/logger.ts index f03985776..f3d1b9135 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -12,7 +12,6 @@ const debugPackage = debugModule(config.get('cli')); /** * Wrapper for debug statements. - * @param {String} input */ function debug(input: string) { /* istanbul ignore next */ @@ -22,7 +21,6 @@ function debug(input: string) { /** * Wrapper for warn statements. - * @param {String} input */ function warn(input: string) { /* istanbul ignore next */ @@ -33,11 +31,14 @@ function warn(input: string) { /** * Wrapper for info/notice statements. - * @param {String} input + * @param {Boolean} includeEmojiPrefix whether or not to prefix + * the statement with this emoji: ā„¹ļø */ -function info(input: string) { +function info(input: string, includeEmojiPrefix = true) { /* istanbul ignore next */ if (isGHA() && process.env.NODE_ENV !== 'test') return core.notice(input); + /* istanbul ignore next */ + if (!includeEmojiPrefix) return console.info(input); // eslint-disable-line no-console // eslint-disable-next-line no-console return console.info(`ā„¹ļø ${input}`); } diff --git a/src/lib/loginFlow.ts b/src/lib/loginFlow.ts new file mode 100644 index 000000000..21a9392c4 --- /dev/null +++ b/src/lib/loginFlow.ts @@ -0,0 +1,86 @@ +import chalk from 'chalk'; +import config from 'config'; +import isEmail from 'validator/lib/isEmail'; + +import configStore from './configstore'; +import fetch, { handleRes } from './fetch'; +import { debug } from './logger'; +import promptTerminal from './promptWrapper'; + +type LoginBody = { + email?: string; + password?: string; + project?: string; + token?: string; +}; + +function loginFetch(body: LoginBody) { + return fetch(`${config.get('host')}/api/v1/login`, { + method: 'post', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +/** + * The prompt flow for logging a user in and writing the credentials to + * `configstore`. This is a separate lib function because we reuse it both + * in the `login` command as well as any time a user omits an API key. + * @returns A Promise-wrapped string with the logged-in user's credentials + */ +export default async function loginFlow() { + const { email, password, project } = await promptTerminal([ + { + type: 'text', + name: 'email', + message: 'What is your email address?', + initial: configStore.get('email'), + validate(val) { + return isEmail(val) ? true : 'Please provide a valid email address.'; + }, + }, + { + type: 'invisible', + name: 'password', + message: 'What is your password?', + }, + { + type: 'text', + name: 'project', + message: 'What project are you logging into?', + initial: configStore.get('project'), + }, + ]); + + if (!project) { + return Promise.reject(new Error('No project subdomain provided. Please use `--project`.')); + } + + if (!isEmail(email)) { + return Promise.reject(new Error('You must provide a valid email address.')); + } + + return loginFetch({ email, password, project }) + .then(handleRes) + .catch(async err => { + // if the user's login requires 2FA, let's prompt them for the token! + if (err.code === 'LOGIN_TWOFACTOR') { + debug('2FA error response, prompting for 2FA code'); + const { token } = await promptTerminal({ + type: 'text', + name: 'token', + message: 'What is your 2FA token?', + }); + + return loginFetch({ email, password, project, token }).then(handleRes); + } + throw err; + }) + .then(res => { + configStore.set('apiKey', res.apiKey); + configStore.set('email', email); + configStore.set('project', project); + + return `Successfully logged in as ${chalk.green(email)} to the ${chalk.blue(project)} project.`; + }); +}