Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(docs): directory sync fix + test bed rewrites #421

Merged
merged 11 commits into from
Jan 4, 2022
164 changes: 74 additions & 90 deletions __tests__/cmds/docs.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
const nock = require('nock');
const chalk = require('chalk');
const config = require('config');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const frontMatter = require('gray-matter');

const APIError = require('../../src/lib/apiError');

const docs = require('../../src/cmds/docs');
const docsEdit = require('../../src/cmds/docs/edit');

Expand Down Expand Up @@ -186,18 +189,21 @@ describe('rdme docs', () => {
});
});

it('should create only valid docs', () => {
console.log = jest.fn();
expect.assertions(2);

it('should fail if any docs are invalid', async () => {
const folder = 'failure-docs';
const slug = 'fail-doc';
const slugTwo = 'new-doc';

const doc = frontMatter(fs.readFileSync(path.join(fixturesDir, `/failure-docs/${slug}.md`)));
const docTwo = frontMatter(fs.readFileSync(path.join(fixturesDir, `/failure-docs/${slugTwo}.md`)));
const errorObject = {
error: 'DOC_INVALID',
message: "We couldn't save this doc (Path `category` is required.).",
};

const doc = frontMatter(fs.readFileSync(path.join(fixturesDir, `/${folder}/${slug}.md`)));
const docTwo = frontMatter(fs.readFileSync(path.join(fixturesDir, `/${folder}/${slugTwo}.md`)));

const hash = hashFileContents(fs.readFileSync(path.join(fixturesDir, `/failure-docs/${slug}.md`)));
const hashTwo = hashFileContents(fs.readFileSync(path.join(fixturesDir, `/failure-docs/${slugTwo}.md`)));
const hash = hashFileContents(fs.readFileSync(path.join(fixturesDir, `/${folder}/${slug}.md`)));
const hashTwo = hashFileContents(fs.readFileSync(path.join(fixturesDir, `/${folder}/${slugTwo}.md`)));

const getMocks = getNockWithVersionHeader(version)
.get(`/api/v1/docs/${slug}`)
Expand Down Expand Up @@ -238,43 +244,27 @@ describe('rdme docs', () => {
})
.post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash })
.basicAuth({ user: key })
.reply(400, {
error: 'DOC_INVALID',
message: "We couldn't save this doc (Path `category` is required.).",
});
.reply(400, errorObject);

const versionMock = nock(config.host)
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });

return docs.run({ folder: './__tests__/__fixtures__/failure-docs', key, version }).then(message => {
expect(console.log).toHaveBeenCalledTimes(1);
expect(message).toStrictEqual([
{
metadata: { image: [], title: '', description: '' },
api: {
method: 'post',
url: '',
auth: 'required',
params: [],
apiSetting,
},
title: 'This is the document title',
updates: [],
type: 'endpoint',
slug: slugTwo,
body: 'Body',
category,
},
]);
const fullDirectory = `__tests__/__fixtures__/${folder}`;

getMocks.done();
postMocks.done();
versionMock.done();
const formattedErrorObject = {
...errorObject,
message: `Error uploading ${chalk.underline(`${fullDirectory}/${slug}.md`)}:\n\n${errorObject.message}`,
};

console.log.mockRestore();
});
await expect(docs.run({ folder: `./${fullDirectory}`, key, version })).rejects.toStrictEqual(
new APIError(formattedErrorObject)
);

getMocks.done();
postMocks.done();
versionMock.done();
});
});

Expand Down Expand Up @@ -314,14 +304,6 @@ describe('rdme docs', () => {
});

describe('rdme docs:edit', () => {
beforeEach(() => {
console.log = jest.fn();
});

afterEach(() => {
console.log.mockRestore();
});

it('should error if no api key provided', () => {
return expect(docsEdit.run({})).rejects.toThrow('No project API key provided. Please use `--key`.');
});
Expand All @@ -332,8 +314,9 @@ describe('rdme docs:edit', () => {
);
});

it('should fetch the doc from the api', () => {
expect.assertions(4);
it('should fetch the doc from the api', async () => {
expect.assertions(5);
console.info = jest.fn();
const slug = 'getting-started';
const body = 'abcdef';
const edits = 'ghijkl';
Expand Down Expand Up @@ -363,55 +346,55 @@ describe('rdme docs:edit', () => {
fs.appendFile(filename, edits, cb.bind(null, 0));
}

return docsEdit.run({ slug, key, version: '1.0.0', mockEditor }).then(() => {
getMock.done();
putMock.done();
versionMock.done();
expect(fs.existsSync(`${slug}.md`)).toBe(false);
await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).resolves.toBeUndefined();

expect(console.log).toHaveBeenCalledWith('Doc successfully updated. Cleaning up local file.');
});
getMock.done();
putMock.done();
versionMock.done();

expect(fs.existsSync(`${slug}.md`)).toBe(false);
expect(console.info).toHaveBeenCalledWith('Doc successfully updated. Cleaning up local file.');
return console.info.mockRestore();
});

it('should error if remote doc does not exist', () => {
expect.assertions(2);
it('should error if remote doc does not exist', async () => {
const slug = 'no-such-doc';

const getMock = nock(config.host)
.get(`/api/v1/docs/${slug}`)
.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 errorObject = {
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 getMock = nock(config.host).get(`/api/v1/docs/${slug}`).reply(404, errorObject);

const versionMock = nock(config.host)
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });

return docsEdit.run({ slug, key, version: '1.0.0' }).catch(err => {
getMock.done();
versionMock.done();
expect(err.code).toBe('DOC_NOTFOUND');
expect(err.message).toContain("The doc with the slug 'no-such-doc' couldn't be found");
});
await expect(docsEdit.run({ slug, key, version: '1.0.0' })).rejects.toThrow(new APIError(errorObject));

getMock.done();
return versionMock.done();
});

it('should error if doc fails validation', () => {
it('should error if doc fails validation', async () => {
expect.assertions(2);
const slug = 'getting-started';
const body = 'abcdef';

const getMock = nock(config.host).get(`/api/v1/docs/${slug}`).reply(200, { body });

const putMock = nock(config.host).put(`/api/v1/docs/${slug}`).reply(400, {
const errorObject = {
error: 'DOC_INVALID',
message: "We couldn't save this doc ({error})",
message: `We couldn't save this doc (${slug})`,
suggestion: '...a suggestion to resolve the issue...',
help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".',
});
};

const getMock = nock(config.host).get(`/api/v1/docs/${slug}`).reply(200, { body });

const putMock = nock(config.host).put(`/api/v1/docs/${slug}`).reply(400, errorObject);

const versionMock = nock(config.host)
.get(`/api/v1/version/${version}`)
Expand All @@ -422,17 +405,17 @@ describe('rdme docs:edit', () => {
return cb(0);
}

return docsEdit.run({ slug, key, version: '1.0.0', mockEditor }).catch(err => {
expect(err.code).toBe('DOC_INVALID');
getMock.done();
putMock.done();
versionMock.done();
expect(fs.existsSync(`${slug}.md`)).toBe(true);
fs.unlinkSync(`${slug}.md`);
});
await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).rejects.toThrow(new APIError(errorObject));

getMock.done();
putMock.done();
versionMock.done();

expect(fs.existsSync(`${slug}.md`)).toBe(true);
fs.unlinkSync(`${slug}.md`);
});

it('should handle error if $EDITOR fails', () => {
it('should handle error if $EDITOR fails', async () => {
expect.assertions(1);
const slug = 'getting-started';
const body = 'abcdef';
Expand All @@ -448,10 +431,11 @@ describe('rdme docs:edit', () => {
return cb(1);
}

return docsEdit.run({ slug, key, version: '1.0.0', mockEditor }).catch(err => {
getMock.done();
expect(err.message).toBe('Non zero exit code from $EDITOR');
fs.unlinkSync(`${slug}.md`);
});
await expect(docsEdit.run({ slug, key, version: '1.0.0', mockEditor })).rejects.toThrow(
new Error('Non zero exit code from $EDITOR')
);

getMock.done();
fs.unlinkSync(`${slug}.md`);
});
});
8 changes: 7 additions & 1 deletion src/cmds/docs/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,17 @@ exports.run = async function (opts) {
})
.then(res => res.json())
.then(async res => {
// The reason we aren't using our handleRes() function here is
// because we need to use the `reject` function from
// the Promise that's wrapping this function.
if (res.error) {
return reject(new APIError(res));
}
console.log(`Doc successfully updated. Cleaning up local file.`);
console.info(`Doc successfully updated. Cleaning up local file.`);
await unlink(filename);
// Normally we should resolve with a value that is logged to the console,
// but since we need to wait for the temporary file to be removed,
// it's okay to resolve the promise with no value.
return resolve();
});
});
Expand Down
22 changes: 8 additions & 14 deletions src/cmds/docs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ exports.run = async function (opts) {
return Promise.reject(new Error(`No folder provided. Usage \`${config.cli} ${exports.usage}\`.`));
}

const selectedVersion = await getProjectVersion(version, key, true).catch(e => {
return Promise.reject(e);
});
// TODO: should we allow version selection at all here?
// Let's revisit this once we re-evaluate our category logic in the API.
// Ideally we should ignore this parameter entirely if the category is included.
const selectedVersion = await getProjectVersion(version, key, false);

// Find the files to sync
const readdirRecursive = folderToSearch => {
Expand Down Expand Up @@ -110,7 +111,7 @@ exports.run = async function (opts) {
}).then(res => handleRes(res));
}

const updatedDocs = await Promise.allSettled(
const updatedDocs = await Promise.all(
files.map(async filename => {
const file = await readFile(filename, 'utf8');
const matter = frontMatter(file);
Expand All @@ -134,19 +135,12 @@ exports.run = async function (opts) {
return updateDoc(slug, matter, hash, res);
})
.catch(err => {
console.log(chalk.red(`\n\`${slug}\` failed to upload. ${err.message}\n`));
// eslint-disable-next-line no-param-reassign
err.message = `Error uploading ${chalk.underline(filename)}:\n\n${err.message}`;
throw err;
});
})
);

for (let i = 0; i < updatedDocs.length; ) {
if (updatedDocs[i].value !== undefined) {
updatedDocs[i] = updatedDocs[i].value; // returns only the value of the response
i += 1;
} else {
updatedDocs.splice(i, 1); // we already displayed the error messages so we can filter those out
}
}

return updatedDocs;
};
2 changes: 1 addition & 1 deletion src/lib/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports.generatePrompts = (versionList, selectOnly = false) => [
{
type: 'select',
name: 'option',
message: 'Would you like to use an existing version or create a new one to associate with your OAS file?',
Copy link
Member Author

@kanadgupta kanadgupta Jan 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were previously using this OAS-specific language in a bunch of places (version commands, docs commands, etc.) 😬

message: 'Would you like to use an existing project version or create a new one?',
skip() {
return selectOnly;
},
Expand Down