diff --git a/__tests__/.eslintrc b/__tests__/.eslintrc index 727df504a..b99d491e0 100644 --- a/__tests__/.eslintrc +++ b/__tests__/.eslintrc @@ -1,20 +1,3 @@ { - "extends": ["@readme/eslint-config/testing"], - "env": { - "jest": true - }, - "rules": { - "jest/expect-expect": [ - "error", - { - "assertFunctionNames": [ - "expect", - "getNockWithVersionHeader.**.reply", - "nock.**.reply" - ] - } - ], - - "jest/no-conditional-expect": "off" - } + "extends": ["@readme/eslint-config/testing"] } diff --git a/__tests__/cmds/docs.test.js b/__tests__/cmds/docs.test.js index fe14f22ba..ef8e4561a 100644 --- a/__tests__/cmds/docs.test.js +++ b/__tests__/cmds/docs.test.js @@ -7,6 +7,8 @@ const crypto = require('crypto'); const frontMatter = require('gray-matter'); const APIError = require('../../src/lib/apiError'); +const getApiNock = require('../get-api-nock'); +const { userAgent } = require('../get-api-nock'); const DocsCommand = require('../../src/cmds/docs'); const DocsEditCommand = require('../../src/cmds/docs/edit'); @@ -24,6 +26,7 @@ function getNockWithVersionHeader(v) { return nock(config.get('host'), { reqheaders: { 'x-readme-version': v, + 'User-Agent': userAgent, }, }); } @@ -106,7 +109,7 @@ describe('rdme docs', () => { .basicAuth({ user: key }) .reply(200, { category, slug: anotherDoc.slug, body: anotherDoc.doc.content }); - const versionMock = nock(config.get('host')) + const versionMock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }); @@ -140,7 +143,7 @@ describe('rdme docs', () => { .basicAuth({ user: key }) .reply(200, { category, slug: anotherDoc.slug, lastUpdatedHash: anotherDoc.hash }); - const versionMock = nock(config.get('host')) + const versionMock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }); @@ -158,7 +161,7 @@ describe('rdme docs', () => { }); describe('new docs', () => { - it('should create new doc', () => { + it('should create new doc', async () => { const slug = 'new-doc'; const doc = frontMatter(fs.readFileSync(path.join(fixturesDir, `/new-docs/${slug}.md`))); const hash = hashFileContents(fs.readFileSync(path.join(fixturesDir, `/new-docs/${slug}.md`))); @@ -178,16 +181,24 @@ describe('rdme docs', () => { .basicAuth({ user: key }) .reply(201, { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - const versionMock = nock(config.get('host')) + const versionMock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }); - return docs.run({ folder: './__tests__/__fixtures__/new-docs', key, version }).then(() => { - getMock.done(); - postMock.done(); - versionMock.done(); - }); + await expect(docs.run({ folder: './__tests__/__fixtures__/new-docs', key, version })).resolves.toStrictEqual([ + { + slug: 'new-doc', + body: '\nBody\n', + category: '5ae122e10fdf4e39bb34db6f', + title: 'This is the document title', + lastUpdatedHash: 'a23046c1e9d8ab47f8875ae7c5e429cb95be1c48', + }, + ]); + + getMock.done(); + postMock.done(); + versionMock.done(); }); it('should fail if any docs are invalid', async () => { @@ -247,7 +258,7 @@ describe('rdme docs', () => { .basicAuth({ user: key }) .reply(400, errorObject); - const versionMock = nock(config.get('host')) + const versionMock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }); @@ -270,12 +281,12 @@ describe('rdme docs', () => { }); describe('slug metadata', () => { - it('should use provided slug', () => { + it('should use provided slug', async () => { const slug = 'new-doc-slug'; const doc = frontMatter(fs.readFileSync(path.join(fixturesDir, `/slug-docs/${slug}.md`))); const hash = hashFileContents(fs.readFileSync(path.join(fixturesDir, `/slug-docs/${slug}.md`))); - const getMock = getNockWithVersionHeader(version) + const getMock = getApiNock() .get(`/api/v1/docs/${doc.data.slug}`) .basicAuth({ user: key }) .reply(404, { @@ -285,21 +296,29 @@ describe('rdme docs', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }); - const postMock = getNockWithVersionHeader(version) + const postMock = getApiNock() .post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }) .basicAuth({ user: key }) .reply(201, { slug: doc.data.slug, body: doc.content, ...doc.data, lastUpdatedHash: hash }); - const versionMock = nock(config.get('host')) + const versionMock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }); - return docs.run({ folder: './__tests__/__fixtures__/slug-docs', key, version }).then(() => { - getMock.done(); - postMock.done(); - versionMock.done(); - }); + await expect(docs.run({ folder: './__tests__/__fixtures__/slug-docs', key, version })).resolves.toStrictEqual([ + { + slug: 'marc-actually-wrote-a-test', + body: '\nBody\n', + category: 'CATEGORY_ID', + title: 'This is the document title', + lastUpdatedHash: 'c9cb7cc26e90775548e1d182ae7fcaa0eaba96bc', + }, + ]); + + getMock.done(); + postMock.done(); + versionMock.done(); }); }); }); @@ -336,10 +355,7 @@ describe('rdme docs:edit', () => { .basicAuth({ user: key }) .reply(200, { category, slug }); - const versionMock = nock(config.get('host')) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); + const versionMock = getApiNock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); function mockEditor(filename, cb) { expect(filename).toBe(`${slug}.md`); @@ -368,12 +384,9 @@ describe('rdme docs:edit', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const getMock = nock(config.get('host')).get(`/api/v1/docs/${slug}`).reply(404, errorObject); + const getMock = getApiNock().get(`/api/v1/docs/${slug}`).reply(404, errorObject); - const versionMock = nock(config.get('host')) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); + const versionMock = getApiNock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); await expect(docsEdit.run({ slug, key, version: '1.0.0' })).rejects.toThrow(new APIError(errorObject)); @@ -392,14 +405,9 @@ describe('rdme docs:edit', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const getMock = nock(config.get('host')).get(`/api/v1/docs/${slug}`).reply(200, { body }); - - const putMock = nock(config.get('host')).put(`/api/v1/docs/${slug}`).reply(400, errorObject); - - const versionMock = nock(config.get('host')) - .get(`/api/v1/version/${version}`) - .basicAuth({ user: key }) - .reply(200, { version }); + const getMock = getApiNock().get(`/api/v1/docs/${slug}`).reply(200, { body }); + const putMock = getApiNock().put(`/api/v1/docs/${slug}`).reply(400, errorObject); + const versionMock = getApiNock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version }); function mockEditor(filename, cb) { return cb(0); @@ -419,7 +427,7 @@ describe('rdme docs:edit', () => { const slug = 'getting-started'; const body = 'abcdef'; - const getMock = nock(config.get('host')) + const getMock = getApiNock() .get(`/api/v1/docs/${slug}`) .reply(200, { body }) .get(`/api/v1/version/${version}`) diff --git a/__tests__/cmds/login.test.js b/__tests__/cmds/login.test.js index c3b4a8bbe..ac3067d18 100644 --- a/__tests__/cmds/login.test.js +++ b/__tests__/cmds/login.test.js @@ -1,8 +1,8 @@ const nock = require('nock'); -const config = require('config'); const configStore = require('../../src/lib/configstore'); const Command = require('../../src/cmds/login'); const APIError = require('../../src/lib/apiError'); +const getApiNock = require('../get-api-nock'); const cmd = new Command(); @@ -32,7 +32,7 @@ describe('rdme login', () => { it('should post to /login on the API', async () => { const apiKey = 'abcdefg'; - const mock = nock(config.get('host')).post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); + const mock = getApiNock().post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); await expect(cmd.run({ email, password, project })).resolves.toMatchSnapshot(); @@ -52,7 +52,7 @@ describe('rdme login', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mock = nock(config.get('host')).post('/api/v1/login', { email, password, project }).reply(401, errorResponse); + const mock = getApiNock().post('/api/v1/login', { email, password, project }).reply(401, errorResponse); await expect(cmd.run({ email, password, project })).rejects.toStrictEqual(new APIError(errorResponse)); mock.done(); @@ -66,7 +66,7 @@ describe('rdme login', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mock = nock(config.get('host')).post('/api/v1/login', { email, password, project }).reply(401, errorResponse); + const mock = getApiNock().post('/api/v1/login', { email, password, project }).reply(401, errorResponse); await expect(cmd.run({ email, password, project })).rejects.toStrictEqual(new APIError(errorResponse)); mock.done(); @@ -75,9 +75,7 @@ describe('rdme login', () => { it('should send 2fa token if provided', async () => { const token = '123456'; - const mock = nock(config.get('host')) - .post('/api/v1/login', { email, password, project, token }) - .reply(200, { apiKey: '123' }); + const mock = getApiNock().post('/api/v1/login', { email, password, project, token }).reply(200, { apiKey: '123' }); await expect(cmd.run({ email, password, project, token })).resolves.toMatchSnapshot(); mock.done(); diff --git a/__tests__/cmds/openapi.test.js b/__tests__/cmds/openapi.test.js index 857a0432c..37c70ebe7 100644 --- a/__tests__/cmds/openapi.test.js +++ b/__tests__/cmds/openapi.test.js @@ -6,6 +6,7 @@ const promptHandler = require('../../src/lib/prompts'); const SwaggerCommand = require('../../src/cmds/swagger'); const OpenAPICommand = require('../../src/cmds/openapi'); const APIError = require('../../src/lib/apiError'); +const getApiNock = require('../get-api-nock'); const openapi = new OpenAPICommand(); const swagger = new SwaggerCommand(); @@ -62,7 +63,7 @@ describe('rdme openapi', () => { ['OpenAPI 3.1', 'json', '3.1'], ['OpenAPI 3.1', 'yaml', '3.1'], ])('should support uploading a %s definition (format: %s)', async (_, format, specVersion) => { - const mock = nock(config.get('host')) + const mock = getApiNock() .get('/api/v1/api-specification') .basicAuth({ user: key }) .reply(200, []) @@ -89,7 +90,7 @@ describe('rdme openapi', () => { it('should discover and upload an API definition if none is provided', async () => { promptHandler.createOasPrompt.mockResolvedValue({ option: 'create' }); - const mock = nock(config.get('host')) + const mock = getApiNock() .get('/api/v1/version') .basicAuth({ user: key }) .reply(200, [{ version }]) @@ -130,7 +131,7 @@ describe('rdme openapi', () => { ['OpenAPI 3.1', 'json', '3.1'], ['OpenAPI 3.1', 'yaml', '3.1'], ])('should support updating a %s definition (format: %s)', async (_, format, specVersion) => { - const mock = nock(config.get('host')) + const mock = getApiNock() .put(`/api/v1/api-specification/${id}`, body => body.match('form-data; name="spec"')) .basicAuth({ user: key }) .reply(201, { _id: 1 }, { location: exampleRefLocation }); @@ -148,7 +149,7 @@ describe('rdme openapi', () => { }); it('should still support `token`', async () => { - const mock = nock(config.get('host')) + const mock = getApiNock() .put(`/api/v1/api-specification/${id}`, body => body.match('form-data; name="spec"')) .basicAuth({ user: key }) .reply(201, { _id: 1 }, { location: exampleRefLocation }); @@ -172,7 +173,7 @@ describe('rdme openapi', () => { }); it('should return warning if providing `id` and `version`', async () => { - const mock = nock(config.get('host')) + const mock = getApiNock() .put(`/api/v1/api-specification/${id}`, body => body.match('form-data; name="spec"')) .basicAuth({ user: key }) .reply(201, { _id: 1 }, { location: exampleRefLocation }); @@ -211,7 +212,7 @@ describe('rdme openapi', () => { ], }; - const mock = nock(config.get('host')).get(`/api/v1/version/${invalidVersion}`).reply(404, errorObject); + const mock = getApiNock().get(`/api/v1/version/${invalidVersion}`).reply(404, errorObject); await expect( openapi.run({ @@ -230,7 +231,7 @@ describe('rdme openapi', () => { newVersion: '1.0.1', }); - const mock = nock(config.get('host')) + const mock = getApiNock() .get('/api/v1/version') .basicAuth({ user: key }) .reply(200, [{ version: '1.0.0' }]) @@ -254,7 +255,7 @@ describe('rdme openapi', () => { it('should bundle and upload the expected content', async () => { let requestBody = null; - const mock = nock(config.get('host')) + const mock = getApiNock() .get('/api/v1/api-specification') .basicAuth({ user: key }) .reply(200, []) @@ -304,7 +305,7 @@ describe('rdme openapi', () => { ], }; - const mock = nock(config.get('host')).get('/api/v1/version').reply(401, errorObject); + const mock = getApiNock().get('/api/v1/version').reply(401, errorObject); await expect( openapi.run({ key, spec: require.resolve('@readme/oas-examples/3.1/json/petstore.json') }) @@ -314,7 +315,7 @@ describe('rdme openapi', () => { }); it('should error if no file was provided or able to be discovered', async () => { - const mock = nock(config.get('host')) + const mock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version: '1.0.0' }); @@ -344,7 +345,7 @@ describe('rdme openapi', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mock = nock(config.get('host')) + const mock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version: '1.0.0' }) @@ -376,7 +377,7 @@ describe('rdme openapi', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mock = nock(config.get('host')) + const mock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version: '1.0.0' }) @@ -396,7 +397,7 @@ describe('rdme openapi', () => { }); it('should error if API errors (generic upload error)', async () => { - const mock = nock(config.get('host')) + const mock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version: '1.0.0' }) @@ -416,7 +417,7 @@ describe('rdme openapi', () => { }); it('should error if API errors (request timeout)', async () => { - const mock = nock(config.get('host')) + const mock = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version: '1.0.0' }) diff --git a/__tests__/cmds/versions.test.js b/__tests__/cmds/versions.test.js index f6100855f..791864725 100644 --- a/__tests__/cmds/versions.test.js +++ b/__tests__/cmds/versions.test.js @@ -1,7 +1,7 @@ const nock = require('nock'); -const config = require('config'); const promptHandler = require('../../src/lib/prompts'); const APIError = require('../../src/lib/apiError'); +const getApiNock = require('../get-api-nock'); const VersionsCommand = require('../../src/cmds/versions'); const CreateVersionCommand = require('../../src/cmds/versions/create'); @@ -49,7 +49,7 @@ describe('rdme versions*', () => { }); it('should make a request to get a list of existing versions', async () => { - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get('/api/v1/version') .basicAuth({ user: key }) .reply(200, [versionPayload, version2Payload]); @@ -61,7 +61,7 @@ describe('rdme versions*', () => { }); it('should make a request to get a list of existing versions and return them in a raw format', async () => { - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get('/api/v1/version') .basicAuth({ user: key }) .reply(200, [versionPayload, version2Payload]); @@ -72,7 +72,7 @@ describe('rdme versions*', () => { }); it('should get a specific version object if version flag provided', async () => { - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, versionPayload); @@ -84,7 +84,7 @@ describe('rdme versions*', () => { }); it('should get a specific version object if version flag provided and return it in a raw format', async () => { - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, versionPayload); @@ -99,19 +99,17 @@ describe('rdme versions*', () => { const createVersion = new CreateVersionCommand(); it('should error if no api key provided', () => { - return createVersion.run({}).catch(err => { - expect(err.message).toBe('No project API key provided. Please use `--key`.'); - }); + return expect(createVersion.run({})).rejects.toThrow('No project API key provided. Please use `--key`.'); }); - it('should get a specific version object', () => { + it('should create a specific version', async () => { promptHandler.createVersionPrompt.mockResolvedValue({ is_stable: true, is_beta: false, from: '1.0.0', }); - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get('/api/v1/version') .basicAuth({ user: key }) .reply(200, [{ version }, { version }]) @@ -119,9 +117,8 @@ describe('rdme versions*', () => { .basicAuth({ user: key }) .reply(201, { version }); - return createVersion.run({ key, version }).then(() => { - mockRequest.done(); - }); + await expect(createVersion.run({ key, version })).resolves.toBe('Version 1.0.0 created successfully.'); + mockRequest.done(); }); it('should catch any post request errors', async () => { @@ -138,10 +135,7 @@ describe('rdme versions*', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mockRequest = nock(config.get('host')) - .post('/api/v1/version') - .basicAuth({ user: key }) - .reply(400, errorResponse); + const mockRequest = getApiNock().post('/api/v1/version').basicAuth({ user: key }).reply(400, errorResponse); await expect(createVersion.run({ key, version, fork: '0.0.5' })).rejects.toStrictEqual( new APIError(errorResponse) @@ -160,7 +154,7 @@ describe('rdme versions*', () => { }); it('should delete a specific version', async () => { - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .delete(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { removed: true }) @@ -181,7 +175,7 @@ describe('rdme versions*', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .delete(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(404, errorResponse) @@ -210,7 +204,7 @@ describe('rdme versions*', () => { is_deprecated: true, }); - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }) @@ -238,7 +232,7 @@ describe('rdme versions*', () => { help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".', }; - const mockRequest = nock(config.get('host')) + const mockRequest = getApiNock() .get(`/api/v1/version/${version}`) .basicAuth({ user: key }) .reply(200, { version }) diff --git a/__tests__/get-api-nock.js b/__tests__/get-api-nock.js new file mode 100644 index 000000000..451b63a07 --- /dev/null +++ b/__tests__/get-api-nock.js @@ -0,0 +1,15 @@ +const nock = require('nock'); +const config = require('config'); +const pkg = require('../package.json'); + +const userAgent = `rdme/${pkg.version}`; + +module.exports = function () { + return nock(config.get('host'), { + reqheaders: { + 'User-Agent': userAgent, + }, + }); +}; + +module.exports.userAgent = userAgent; diff --git a/__tests__/lib/cleanHeaders.test.js b/__tests__/lib/cleanHeaders.test.js deleted file mode 100644 index 45ac48659..000000000 --- a/__tests__/lib/cleanHeaders.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const { cleanHeaders } = require('../../src/lib/cleanHeaders'); - -describe('cleanHeaders', () => { - it('should b64-encode key in ReadMe-friendly format', () => { - expect(cleanHeaders('test')).toStrictEqual({ Authorization: 'Basic dGVzdDo=' }); - }); - - it('should filter out undefined headers', () => { - expect(cleanHeaders('test', { 'x-readme-version': undefined })).toStrictEqual({ Authorization: 'Basic dGVzdDo=' }); - }); - - it('should filter out null headers', () => { - expect(cleanHeaders('test', { 'x-readme-version': undefined, Accept: null })).toStrictEqual({ - Authorization: 'Basic dGVzdDo=', - }); - }); - - it('should pass in properly defined headers', () => { - expect( - cleanHeaders('test', { 'x-readme-version': undefined, Accept: null, 'Content-Type': 'application/json' }) - ).toStrictEqual({ - Authorization: 'Basic dGVzdDo=', - 'Content-Type': 'application/json', - }); - }); -}); diff --git a/__tests__/lib/commands.test.js b/__tests__/lib/commands.test.js index 1a683dd41..2ecc6c634 100644 --- a/__tests__/lib/commands.test.js +++ b/__tests__/lib/commands.test.js @@ -1,3 +1,4 @@ +/* eslint-disable jest/no-conditional-expect */ const commands = require('../../src/lib/commands'); describe('utils', () => { diff --git a/__tests__/lib/fetch.test.js b/__tests__/lib/fetch.test.js new file mode 100644 index 000000000..3df480fbe --- /dev/null +++ b/__tests__/lib/fetch.test.js @@ -0,0 +1,63 @@ +const config = require('config'); +const fetch = require('../../src/lib/fetch'); +const { cleanHeaders, handleRes } = require('../../src/lib/fetch'); +const getApiNock = require('../get-api-nock'); + +describe('#fetch()', () => { + it('should wrap all requests with a rdme User-Agent', async () => { + const key = 'API_KEY'; + + const mock = getApiNock() + .get('/api/v1') + .basicAuth({ user: key }) + .reply(200, function () { + return this.req.headers['user-agent']; + }); + + const userAgent = await fetch(`${config.get('host')}/api/v1`, { + method: 'get', + headers: cleanHeaders(key), + }).then(handleRes); + + expect(userAgent.shift()).toMatch(/rdme\/\d+.\d+.\d+/); + mock.done(); + }); + + it('should support if we dont supply any other options with the request', async () => { + const mock = getApiNock() + .get('/api/v1/doesnt-need-auth') + .reply(200, function () { + return this.req.headers['user-agent']; + }); + + const userAgent = await fetch(`${config.get('host')}/api/v1/doesnt-need-auth`).then(handleRes); + + expect(userAgent.shift()).toMatch(/rdme\/\d+.\d+.\d+/); + mock.done(); + }); +}); + +describe('#cleanHeaders()', () => { + it('should base64-encode key in ReadMe-friendly format', () => { + expect(cleanHeaders('test')).toStrictEqual({ Authorization: 'Basic dGVzdDo=' }); + }); + + it('should filter out undefined headers', () => { + expect(cleanHeaders('test', { 'x-readme-version': undefined })).toStrictEqual({ Authorization: 'Basic dGVzdDo=' }); + }); + + it('should filter out null headers', () => { + expect(cleanHeaders('test', { 'x-readme-version': undefined, Accept: null })).toStrictEqual({ + Authorization: 'Basic dGVzdDo=', + }); + }); + + it('should pass in properly defined headers', () => { + expect( + cleanHeaders('test', { 'x-readme-version': undefined, Accept: null, 'Content-Type': 'application/json' }) + ).toStrictEqual({ + Authorization: 'Basic dGVzdDo=', + 'Content-Type': 'application/json', + }); + }); +}); diff --git a/__tests__/lib/prompts.test.js b/__tests__/lib/prompts.test.js index 1e5ae1de4..45cc46083 100644 --- a/__tests__/lib/prompts.test.js +++ b/__tests__/lib/prompts.test.js @@ -48,6 +48,7 @@ describe('prompt test bed', () => { break; case 'versionSelection': + // eslint-disable-next-line jest/no-conditional-expect await expect(prompt.skip()).resolves.toBe(true); break; diff --git a/package.json b/package.json index 00d96409d..905a80870 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "./__tests__/set-node-env" ], "testPathIgnorePatterns": [ + "./__tests__/get-api-nock", "./__tests__/set-node-env" ] }, diff --git a/src/cmds/docs/edit.js b/src/cmds/docs/edit.js index 1b94474c6..a42ab78e9 100644 --- a/src/cmds/docs/edit.js +++ b/src/cmds/docs/edit.js @@ -3,10 +3,9 @@ const fs = require('fs'); const editor = require('editor'); const { promisify } = require('util'); const APIError = require('../../lib/apiError'); -const { cleanHeaders } = require('../../lib/cleanHeaders'); const { getProjectVersion } = require('../../lib/versionSelect'); -const { handleRes } = require('../../lib/handleRes'); -const fetch = require('node-fetch'); +const fetch = require('../../lib/fetch'); +const { cleanHeaders, handleRes } = require('../../lib/fetch'); const writeFile = promisify(fs.writeFile); const readFile = promisify(fs.readFile); diff --git a/src/cmds/docs/index.js b/src/cmds/docs/index.js index 91a1f7632..729811a8a 100644 --- a/src/cmds/docs/index.js +++ b/src/cmds/docs/index.js @@ -5,10 +5,9 @@ const config = require('config'); const crypto = require('crypto'); const frontMatter = require('gray-matter'); const { promisify } = require('util'); -const { cleanHeaders } = require('../../lib/cleanHeaders'); const { getProjectVersion } = require('../../lib/versionSelect'); -const { handleRes } = require('../../lib/handleRes'); -const fetch = require('node-fetch'); +const fetch = require('../../lib/fetch'); +const { cleanHeaders, handleRes } = require('../../lib/fetch'); const readFile = promisify(fs.readFile); diff --git a/src/cmds/login.js b/src/cmds/login.js index 50a3ec0a3..470ccd19a 100644 --- a/src/cmds/login.js +++ b/src/cmds/login.js @@ -4,8 +4,8 @@ const { validate: isEmail } = require('isemail'); const { promisify } = require('util'); const read = promisify(require('read')); const configStore = require('../lib/configstore'); -const { handleRes } = require('../lib/handleRes'); -const fetch = require('node-fetch'); +const fetch = require('../lib/fetch'); +const { handleRes } = require('../lib/fetch'); const testing = process.env.NODE_ENV === 'testing'; diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js index 73c12f784..3db39a003 100644 --- a/src/cmds/openapi.js +++ b/src/cmds/openapi.js @@ -5,9 +5,9 @@ const { prompt } = require('enquirer'); const OASNormalize = require('oas-normalize'); const promptOpts = require('../lib/prompts'); const APIError = require('../lib/apiError'); -const { cleanHeaders } = require('../lib/cleanHeaders'); const { getProjectVersion } = require('../lib/versionSelect'); -const fetch = require('node-fetch'); +const fetch = require('../lib/fetch'); +const { cleanHeaders } = require('../lib/fetch'); const FormData = require('form-data'); const parse = require('parse-link-header'); const { file: tmpFile } = require('tmp-promise'); diff --git a/src/cmds/versions/create.js b/src/cmds/versions/create.js index 940bf9c31..e9b7b6c6d 100644 --- a/src/cmds/versions/create.js +++ b/src/cmds/versions/create.js @@ -2,9 +2,8 @@ const config = require('config'); const semver = require('semver'); const { prompt } = require('enquirer'); const promptOpts = require('../../lib/prompts'); -const { cleanHeaders } = require('../../lib/cleanHeaders'); -const { handleRes } = require('../../lib/handleRes'); -const fetch = require('node-fetch'); +const fetch = require('../../lib/fetch'); +const { cleanHeaders, handleRes } = require('../../lib/fetch'); module.exports = class CreateVersionCommand { constructor() { diff --git a/src/cmds/versions/delete.js b/src/cmds/versions/delete.js index 599e4f01f..b899247d4 100644 --- a/src/cmds/versions/delete.js +++ b/src/cmds/versions/delete.js @@ -1,8 +1,7 @@ const config = require('config'); const { getProjectVersion } = require('../../lib/versionSelect'); -const { cleanHeaders } = require('../../lib/cleanHeaders'); -const { handleRes } = require('../../lib/handleRes'); -const fetch = require('node-fetch'); +const fetch = require('../../lib/fetch'); +const { cleanHeaders, handleRes } = require('../../lib/fetch'); module.exports = class DeleteVersionCommand { constructor() { diff --git a/src/cmds/versions/index.js b/src/cmds/versions/index.js index 22cbbb9f2..dc626c682 100644 --- a/src/cmds/versions/index.js +++ b/src/cmds/versions/index.js @@ -2,9 +2,8 @@ const chalk = require('chalk'); const Table = require('cli-table'); const config = require('config'); const CreateVersionCmd = require('./create'); -const { cleanHeaders } = require('../../lib/cleanHeaders'); -const fetch = require('node-fetch'); -const { handleRes } = require('../../lib/handleRes'); +const fetch = require('../../lib/fetch'); +const { cleanHeaders, handleRes } = require('../../lib/fetch'); module.exports = class VersionsCommand { constructor() { diff --git a/src/cmds/versions/update.js b/src/cmds/versions/update.js index 71b44223c..c1fc92a0b 100644 --- a/src/cmds/versions/update.js +++ b/src/cmds/versions/update.js @@ -1,10 +1,9 @@ const config = require('config'); const { prompt } = require('enquirer'); const promptOpts = require('../../lib/prompts'); -const { cleanHeaders } = require('../../lib/cleanHeaders'); const { getProjectVersion } = require('../../lib/versionSelect'); -const fetch = require('node-fetch'); -const { handleRes } = require('../../lib/handleRes'); +const fetch = require('../../lib/fetch'); +const { cleanHeaders, handleRes } = require('../../lib/fetch'); module.exports = class UpdateVersionCommand { constructor() { diff --git a/src/lib/cleanHeaders.js b/src/lib/cleanHeaders.js deleted file mode 100644 index 5f1b0786a..000000000 --- a/src/lib/cleanHeaders.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Returns the basic auth header and any other defined headers for use in node-fetch API calls. - * @param {string} key The ReadMe project API key - * @param {Object} inputHeaders Any additional headers to be cleaned - * @returns An object with cleaned request headers for usage in the node-fetch requests to the ReadMe API. - */ -function cleanHeaders(key, inputHeaders = {}) { - const encodedKey = Buffer.from(`${key}:`).toString('base64'); - const headers = { - Authorization: `Basic ${encodedKey}`, - }; - - Object.keys(inputHeaders).forEach(header => { - // For some reason, node-fetch will send in the string 'undefined' - // if you pass in an undefined value for a header, - // so that's why headers are added incrementally. - if (typeof inputHeaders[header] === 'string') headers[header] = inputHeaders[header]; - }); - - return headers; -} - -module.exports = { cleanHeaders }; diff --git a/src/lib/fetch.js b/src/lib/fetch.js new file mode 100644 index 000000000..332cce133 --- /dev/null +++ b/src/lib/fetch.js @@ -0,0 +1,59 @@ +/* eslint-disable no-param-reassign */ +const fetch = require('node-fetch'); +const pkg = require('../../package.json'); +const APIError = require('./apiError'); + +/** + * Wrapper for the `fetch` API so we can add an rdme user agent to all API requests. + * + */ +module.exports = (url, options = {}) => { + const userAgent = `rdme/${pkg.version}`; + + if (!options.headers) { + options.headers = { + 'User-Agent': userAgent, + }; + } else { + options.headers['User-Agent'] = userAgent; + } + + return fetch(url, options); +}; + +/** + * Small handler for transforming responses from our API into JSON and if there's errors, throwing + * an APIError exception. + * + * @param {Response} res + */ +module.exports.handleRes = async function handleRes(res) { + const body = await res.json(); + if (body.error) { + return Promise.reject(new APIError(body)); + } + return body; +}; + +/** + * Returns the basic auth header and any other defined headers for use in node-fetch API calls. + * + * @param {string} key The ReadMe project API key + * @param {Object} inputHeaders Any additional headers to be cleaned + * @returns An object with cleaned request headers for usage in the node-fetch requests to the ReadMe API. + */ +module.exports.cleanHeaders = function cleanHeaders(key, inputHeaders = {}) { + const encodedKey = Buffer.from(`${key}:`).toString('base64'); + const headers = { + Authorization: `Basic ${encodedKey}`, + }; + + Object.keys(inputHeaders).forEach(header => { + // For some reason, node-fetch will send in the string 'undefined' + // if you pass in an undefined value for a header, + // so that's why headers are added incrementally. + if (typeof inputHeaders[header] === 'string') headers[header] = inputHeaders[header]; + }); + + return headers; +}; diff --git a/src/lib/handleRes.js b/src/lib/handleRes.js deleted file mode 100644 index e29ede3fd..000000000 --- a/src/lib/handleRes.js +++ /dev/null @@ -1,11 +0,0 @@ -const APIError = require('./apiError'); - -async function handleRes(res) { - const body = await res.json(); - if (body.error) { - return Promise.reject(new APIError(body)); - } - return body; -} - -module.exports = { handleRes }; diff --git a/src/lib/versionSelect.js b/src/lib/versionSelect.js index b1c5c2d31..f33edcf61 100644 --- a/src/lib/versionSelect.js +++ b/src/lib/versionSelect.js @@ -1,10 +1,9 @@ const { prompt } = require('enquirer'); const promptOpts = require('./prompts'); -const { cleanHeaders } = require('./cleanHeaders'); -const fetch = require('node-fetch'); const config = require('config'); const APIError = require('./apiError'); -const { handleRes } = require('./handleRes'); +const fetch = require('./fetch'); +const { cleanHeaders, handleRes } = require('./fetch'); async function getProjectVersion(versionFlag, key, allowNewVersion) { try {