From 4389ce7f0fb56f46c9f98c631b7ada90a9b65da0 Mon Sep 17 00:00:00 2001 From: Javier Sierra Blazquez Date: Mon, 30 Oct 2023 15:07:52 +0000 Subject: [PATCH] feat: update addToGitignore functionality - Modularize isInGitignore functionality - Move inquirer confirm question to inside addToGitignore - Check isInGitignore before ask user to add to .gitignore - Check last file character and add new line if it is necessary - Move add to gitignore call for rc file after saving the config file About testing: - Add tests - Modify check-string-in-file exporting an object, so content can be stubbed - Add a inquirer wrapper in cli file for same previous reason: allow to stub inquirer calls. Closes #21 --- .gitignore | 2 +- src/services/check-string-in-file.js | 3 +- src/services/cli.js | 16 ++++++ src/services/gitignore.js | 43 ++++++++++++++ src/services/user-flow-steps.js | 25 ++------- src/services/utils.js | 22 +------- test/unit/services/gitignore.test.js | 84 ++++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 42 deletions(-) create mode 100644 src/services/cli.js create mode 100644 src/services/gitignore.js create mode 100644 test/unit/services/gitignore.test.js diff --git a/.gitignore b/.gitignore index 9ab8ff5..60cd1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules/ dist-types/ -.api-mock-runner +.api-mock-runner/ .apimockrc coverage/ .DS_Store \ No newline at end of file diff --git a/src/services/check-string-in-file.js b/src/services/check-string-in-file.js index d2e135c..3b8241a 100644 --- a/src/services/check-string-in-file.js +++ b/src/services/check-string-in-file.js @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as readline from 'readline'; -export default async function (stringToCheck, filePath) { +async function check(stringToCheck, filePath) { const reader = readline.createInterface({ input: fs.createReadStream(filePath) }); let exists = false; @@ -14,3 +14,4 @@ export default async function (stringToCheck, filePath) { return exists; } +export const checkStringInFile = { check }; diff --git a/src/services/cli.js b/src/services/cli.js new file mode 100644 index 0000000..8385729 --- /dev/null +++ b/src/services/cli.js @@ -0,0 +1,16 @@ +import * as inquirer from '@inquirer/prompts'; + +/** + * Confirm through CLI adding a file to .gitignore + * @async + * @function confirmAddToGitignore + * @param {string} fileName - The file name to add to .gitignore + * @returns {Promise} True if the user confirms, false otherwise + */ +async function confirmAddToGitignore(fileName) { + return await inquirer.confirm({ + message: `Add ${fileName} to .gitignore?`, + }); +} + +export const cli = { confirmAddToGitignore }; diff --git a/src/services/gitignore.js b/src/services/gitignore.js new file mode 100644 index 0000000..2cc1cd1 --- /dev/null +++ b/src/services/gitignore.js @@ -0,0 +1,43 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { checkStringInFile } from './check-string-in-file.js'; +import { cli } from './cli.js'; + +export const GITIGNORE_PATH = path.join(process.cwd(), '.gitignore'); + +/** + * Append a newline with file or folder name to .gitignore. + * If .gitignore does not exist, it will be created. + * If the file or folder name is already in .gitignore, it will not be added again. + * Any action is confirmed through the CLI by the user. + * @async + * @function addToGitignore + * @param {string} fileName - The file or folder name to append to .gitignore + * @returns {Promise} + */ +export default async function addToGitignore(fileName) { + const existsGitignoreFile = fs.existsSync(GITIGNORE_PATH); + if ((!existsGitignoreFile || !(await isInGitignore(fileName))) && (await cli.confirmAddToGitignore(fileName))) { + const leadingCharacter = existsGitignoreFile ? getLeadingCharacter() : ''; + fs.appendFileSync(GITIGNORE_PATH, `${leadingCharacter}${fileName}\n`); + } +} + +/** + * Check if a string is in .gitignore + * @async + * @function isInGitignore + * @param {string} textToCheck - The text to check + * @returns {Promise} True if the text is in .gitignore, false otherwise + */ +async function isInGitignore(textToCheck) { + const result = await checkStringInFile.check(textToCheck, GITIGNORE_PATH); + return result; +} + +function getLeadingCharacter() { + let leadingCharacter = ''; + const lastFileCharacter = fs.readFileSync(GITIGNORE_PATH, 'utf8').slice(-1); + leadingCharacter = lastFileCharacter === '\n' ? '' : '\n'; + return leadingCharacter; +} diff --git a/src/services/user-flow-steps.js b/src/services/user-flow-steps.js index e60ef88..0c11d51 100644 --- a/src/services/user-flow-steps.js +++ b/src/services/user-flow-steps.js @@ -3,9 +3,9 @@ import * as fs from 'node:fs'; import { OpenApiSchemaNotFoundError } from '../errors/openapi-schema-not-found-error.js'; import cloneGitRepository from '../services/clone-git-repository.js'; import findOasFromDir from '../services/find-oas-from-dir.js'; +import addToGitignore from './gitignore.js'; import { originValidator, portValidator } from './inquirer-validators.js'; -import { RC_FILE_NAME, TEMP_FOLDER_NAME, addToGitignore, verifyRemoteOrigin } from './utils.js'; - +import { RC_FILE_NAME, TEMP_FOLDER_NAME, verifyRemoteOrigin } from './utils.js'; /** * @typedef {Object} Config * @property {string} schemasOrigin - The origin of the schemas (local or remote) @@ -28,24 +28,6 @@ async function initWithConfigFile() { return useExistingConfig ? existingConfig : await init(); } -/** - * first step when the config file doesn't exist - * @async - * @function initNoConfigFile - * @returns {Promise} path or url of the schemas - */ -async function startNewFlow() { - const schemasOrigin = await getOrigin(); - const addRcFileToGitignore = await confirm({ - message: `Add ${RC_FILE_NAME} to .gitignore?`, - }); - if (addRcFileToGitignore) { - await addToGitignore(RC_FILE_NAME); - } - - return schemasOrigin; -} - /** * Get the schemas from the origin * @async @@ -90,7 +72,7 @@ async function getOrigin() { * @throws {OpenApiSchemaNotFoundError} When no schemas are found in the given directory */ async function init({ origin, schemaPaths, ports } = {}) { - const schemasOrigin = origin || (await startNewFlow()); + const schemasOrigin = origin || (await getOrigin()); const schemas = await getSchemas(schemasOrigin); if (!schemas.length) { throw new OpenApiSchemaNotFoundError(); @@ -114,6 +96,7 @@ async function init({ origin, schemaPaths, ports } = {}) { fs.writeFileSync(`${process.cwd()}/${RC_FILE_NAME}`, JSON.stringify(config, null, '\t')); console.log(config); + await addToGitignore(RC_FILE_NAME); return config; } diff --git a/src/services/utils.js b/src/services/utils.js index 6ab9c65..3ff2a79 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -1,6 +1,3 @@ -import * as fs from 'node:fs'; -import checkStringInFile from './check-string-in-file.js'; - /** * The name of the config file * @constant @@ -8,27 +5,15 @@ import checkStringInFile from './check-string-in-file.js'; * @default */ export const RC_FILE_NAME = '.apimockrc'; + /** * The name of the temporary folder * @constant * @type {string} * @default */ -export const TEMP_FOLDER_NAME = '.api-mock-runner'; -/** - * Append text to .gitignore - * @async - * @function addToGitignore - * @param {string} textToAppend - The text to append to .gitignore - * @returns {Promise} - */ -export async function addToGitignore(textToAppend) { - if (!(await checkStringInFile(textToAppend, `${process.cwd()}/.gitignore`))) { - fs.appendFileSync(`${process.cwd()}/.gitignore`, `\n${textToAppend}`); - return true; - } - return false; -} +export const TEMP_FOLDER_NAME = '.api-mock-runner/'; + /** * Verify if the origin is remote * @function verifyRemoteOrigin @@ -51,6 +36,5 @@ export function verifyRemoteOrigin(origin) { export default { RC_FILE_NAME, TEMP_FOLDER_NAME, - addToGitignore, verifyRemoteOrigin, }; diff --git a/test/unit/services/gitignore.test.js b/test/unit/services/gitignore.test.js new file mode 100644 index 0000000..4c6f5e9 --- /dev/null +++ b/test/unit/services/gitignore.test.js @@ -0,0 +1,84 @@ +import { expect, use } from 'chai'; +import fs from 'node:fs'; +import { restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import { checkStringInFile } from '../../../src/services/check-string-in-file.js'; +import { cli } from '../../../src/services/cli.js'; +import addToGitignore, { GITIGNORE_PATH } from '../../../src/services/gitignore.js'; +use(sinonChai); + +describe('unit:addToGitignore', () => { + const gitignoreContentNoNewline = 'fileContentTest'; + const fileNameTest = 'fileNameTest'; + const lineToAdd = `${fileNameTest}\n`; + let appendFileSyncStub; + let checkStringInFileStub; + let confirmStub; + let existsSyncStub; + let readFileSyncStub; + + beforeEach(() => { + appendFileSyncStub = stub(fs, 'appendFileSync'); + checkStringInFileStub = stub(checkStringInFile, 'check'); + confirmStub = stub(cli, 'confirmAddToGitignore'); + existsSyncStub = stub(fs, 'existsSync'); + readFileSyncStub = stub(fs, 'readFileSync'); + }); + + afterEach(() => { + restore(); + }); + + it('should not add filename when already is in it', async () => { + existsSyncStub.returns(true); + checkStringInFileStub.returns(true); + await addToGitignore(fileNameTest); + expect(existsSyncStub).to.have.been.calledWith(GITIGNORE_PATH); + expect(checkStringInFileStub).to.have.been.calledWith(fileNameTest, GITIGNORE_PATH); + expect(confirmStub).to.not.have.been.called; + expect(readFileSyncStub).to.not.have.been.called; + expect(appendFileSyncStub).to.not.have.been.called; + }); + + it('should not add filename when user refuses', async () => { + existsSyncStub.returns(false); + confirmStub.returns(false); + await addToGitignore(fileNameTest); + expect(existsSyncStub).to.have.been.called; + expect(confirmStub).to.have.been.called; + expect(readFileSyncStub).to.not.have.been.called; + expect(appendFileSyncStub).to.not.have.been.called; + }); + + it('should add newline and filename to existing .gitignore when user accepts', async () => { + existsSyncStub.returns(true); + confirmStub.returns(true); + readFileSyncStub.returns(gitignoreContentNoNewline); + await addToGitignore(fileNameTest); + expect(existsSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH); + expect(confirmStub).to.have.been.calledOnceWith(fileNameTest); + expect(readFileSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH, 'utf8'); + expect(appendFileSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH, `\n${lineToAdd}`); + }); + + it('should add filename to existing .gitignore when user accepts', async () => { + existsSyncStub.returns(true); + confirmStub.returns(true); + readFileSyncStub.returns(`${gitignoreContentNoNewline}\n`); + await addToGitignore(fileNameTest); + expect(existsSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH); + expect(confirmStub).to.have.been.calledOnceWith(fileNameTest); + expect(readFileSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH, 'utf8'); + expect(appendFileSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH, `${lineToAdd}`); + }); + + it('should add filename to missing .gitignore when user accepts', async () => { + existsSyncStub.returns(false); + confirmStub.returns(true); + await addToGitignore(fileNameTest); + expect(existsSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH); + expect(confirmStub).to.have.been.calledOnceWith(fileNameTest); + expect(readFileSyncStub).to.not.have.been.called; + expect(appendFileSyncStub).to.have.been.calledOnceWith(GITIGNORE_PATH, `${lineToAdd}`); + }); +});