Skip to content

Commit

Permalink
fix: handle errors reading config
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgecasar committed Feb 2, 2024
1 parent bb8e0bc commit 2e27d0b
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 85 deletions.
41 changes: 41 additions & 0 deletions src/helpers/config-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { cwd } from 'node:process';

import { RC_FILE_NAME } from './constants.js';
import { Logger } from './logger.js';
import { messages } from './messages.js';

/**
* @typedef {import('../types/types.d.js').Config} Config
*/

/**
* Get the config from the config file.
* @returns {Config | null} The config object.
*/
export function getConfigFromFile() {
const filePath = join(cwd(), RC_FILE_NAME);
if (!existsSync(filePath)) {
Logger.warn(messages.CONFIG_FILE_NOT_FOUND, RC_FILE_NAME);
return null;
}
const fileContent = readFileSync(filePath, 'utf-8');
try {
return /** @type {Config} */ (JSON.parse(fileContent)) || null;
} catch (error) {
Logger.error(messages.CONFIG_FILE_INVALID(RC_FILE_NAME));
return null;
}
}

/**
* Save runtime config into rc file.
* @param {Config} config Config object to write.
*/
export function saveConfigToFile(config) {
const filePath = join(cwd(), RC_FILE_NAME);
const fileContent = JSON.stringify(config, null, '\t');
writeFileSync(filePath, fileContent);
Logger.info(messages.SAVED_CONFIG(RC_FILE_NAME), config);
}
1 change: 1 addition & 0 deletions src/helpers/messages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const messages = Object.freeze({
CHOOSE_FILES: 'Choose the files you want to use:',
CONFIG_FILE_NOT_FOUND: 'Could not find the configuration file named:',
CONFIG_FILE_INVALID: (/** @type {string} */ configFile) => `The configuration file ${configFile} is invalid.`,
CONFIRM_ADD_TO_GITIGNORE: (/** @type {string} */ fileName) => `Add ${fileName} to .gitignore?:`,
CONFIRM_EXISTING_CONFIG: 'Do you want to use the existing config?:',
CURRENT_CONFIG: 'Current configuration:',
Expand Down
25 changes: 3 additions & 22 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { program } from 'commander';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { cwd } from 'node:process';

import { RC_FILE_NAME } from './helpers/constants.js';
import { Logger } from './helpers/logger.js';
import { messages } from './helpers/messages.js';
import { getConfigFromFile } from './helpers/config-file.js';
import { startMockServer } from './services/start-mock-server.js';
import { initWithConfigFile } from './services/user-flow-steps/init-with-config-file.js';
import { initWithSchemaPaths } from './services/user-flow-steps/init-with-schema-paths.js';
Expand All @@ -32,14 +27,9 @@ export const main = async () => {
program.parse();
/** @type {ProgramOptions} */
const options = program.opts();
const configFileExists = existsSync(join(cwd(), RC_FILE_NAME));
let config;
if (options.runConfig) {
if (configFileExists) {
config = getConfigFromFile();
} else {
Logger.warn(messages.CONFIG_FILE_NOT_FOUND, RC_FILE_NAME);
}
config = getConfigFromFile();
} else if (options?.origin) {
config = await init({
origin: options.origin,
Expand All @@ -51,20 +41,11 @@ export const main = async () => {
schemaPaths: options.schema,
ports: options.port,
});
} else if (configFileExists) {
} else {
config = await initWithConfigFile();
}
if (!config) {
config = await init();
}
return startMockServer(config.selectedSchemas);
};

/**
* Get config from file.
* @function getConfigFromFile
* @returns {Config} Content of the config file.
*/
function getConfigFromFile() {
return /** @type {Config} */ (JSON.parse(readFileSync(join(cwd(), RC_FILE_NAME), 'utf-8'))) || {};
}
9 changes: 3 additions & 6 deletions src/services/user-flow-steps/helpers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import input from '@inquirer/input';
import { writeFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { cwd } from 'node:process';
import { resolve } from 'node:path';

import { saveConfigToFile } from '../../helpers/config-file.js';
import { RC_FILE_NAME, TEMP_FOLDER_NAME } from '../../helpers/constants.js';
import { Logger } from '../../helpers/logger.js';
import { messages } from '../../helpers/messages.js';
import { verifyRemoteOrigin } from '../../helpers/verify-remote-origin.js';
import { cloneRepository } from '../clone-git-repository.js';
Expand Down Expand Up @@ -103,7 +101,6 @@ export function assignPorts(schemaPaths, ports) {
* @returns {Promise<void>} Promise object represents the void return.
*/
export async function saveRuntimeConfig(config) {
writeFileSync(join(cwd(), RC_FILE_NAME), JSON.stringify(config, null, '\t'));
Logger.info(messages.SAVED_CONFIG(RC_FILE_NAME), config);
saveConfigToFile(config);
await addToGitignore(RC_FILE_NAME);
}
23 changes: 11 additions & 12 deletions src/services/user-flow-steps/init-with-config-file.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import confirm from '@inquirer/confirm';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { cwd } from 'node:process';

import { init } from './init.js';
import { RC_FILE_NAME } from '../../helpers/constants.js';
import { getConfigFromFile } from '../../helpers/config-file.js';
import { Logger } from '../../helpers/logger.js';
import { messages } from '../../helpers/messages.js';

Expand All @@ -19,12 +16,14 @@ import { messages } from '../../helpers/messages.js';
* @returns {Promise<Config>} An object with the initial values from the user.
*/
export async function initWithConfigFile() {
const configFilePath = join(cwd(), RC_FILE_NAME);
const fileContent = readFileSync(configFilePath, 'utf-8');
const existingConfig = /** @type {Config} */ (JSON.parse(fileContent)) || {};
Logger.info(messages.CURRENT_CONFIG, existingConfig);
const useExistingConfig = await confirm({
message: messages.CONFIRM_EXISTING_CONFIG,
});
return useExistingConfig ? existingConfig : init();
const existingConfig = getConfigFromFile();
if (existingConfig) {
Logger.info(messages.CURRENT_CONFIG, existingConfig);
const useExistingConfig = await confirm({
message: messages.CONFIRM_EXISTING_CONFIG,
});
return useExistingConfig ? existingConfig : init();
} else {
return init();
}
}
2 changes: 0 additions & 2 deletions src/services/user-flow-steps/init-with-schema-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import { askForPorts, assignPorts, saveRuntimeConfig } from './helpers.js';
export async function initWithSchemaPaths({ schemaPaths, ports } = { schemaPaths: [], ports: [] }) {
const selectedSchemas = ports?.length ? assignPorts(schemaPaths, ports) : await askForPorts(schemaPaths);
const config = { selectedSchemas };

await saveRuntimeConfig(config);

return config;
}
70 changes: 70 additions & 0 deletions test/unit/helpers/config-file.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, use } from 'chai';
import esmock from 'esmock';
import { createSandbox } from 'sinon';
import sinonChai from 'sinon-chai';

import { RC_FILE_NAME } from '../../../src/helpers/constants.js';
import { messages } from '../../../src/helpers/messages.js';
import { globalMocksFactory } from '../../helpers/global-mocks-factory.js';

use(sinonChai);
const sandbox = createSandbox();

class Logger {
static info = sandbox.stub();
static warn = sandbox.stub();
static error = sandbox.stub();
}

const mocks = {
'./logger.js': { Logger },
};
const globalMocks = globalMocksFactory(sandbox);
const { fs, path } = globalMocks;
const fileToTest = '../../../src/helpers/config-file.js';
const absolutePath = new URL(fileToTest, import.meta.url).pathname;
const { getConfigFromFile, saveConfigToFile } = await esmock(absolutePath, absolutePath, mocks, globalMocks);

describe('unit: config-file', () => {
afterEach(() => {
sandbox.reset();
});

describe('getConfigFromFile', () => {
it('should return null if the config file does not exist', () => {
expect(getConfigFromFile()).to.be.null;
expect(Logger.warn).to.have.been.calledOnceWithExactly(messages.CONFIG_FILE_NOT_FOUND, RC_FILE_NAME);
});

it('should return null if the config file is invalid', () => {
fs.existsSync.returns(true);
fs.readFileSync.returns('invalid json');
expect(getConfigFromFile()).to.be.null;
expect(Logger.error).to.have.been.calledOnceWithExactly(messages.CONFIG_FILE_INVALID(RC_FILE_NAME));
});

it('should return null if the config file is null', () => {
fs.existsSync.returns(true);
fs.readFileSync.returns(null);
expect(getConfigFromFile()).to.be.null;
});

it('should return the config object', () => {
const config = { foo: 'bar' };
fs.existsSync.returns(true);
fs.readFileSync.returns(JSON.stringify(config));
expect(getConfigFromFile()).to.deep.equal(config);
});
});

describe('saveConfigToFile', () => {
it('should write the config to the file', () => {
const configFilePath = '/path/to/config/file';
const config = { foo: 'bar' };
path.join.returns(configFilePath);
saveConfigToFile(config);
expect(fs.writeFileSync).to.have.been.calledOnceWithExactly(configFilePath, JSON.stringify(config, null, '\t'));
expect(Logger.info).to.have.been.calledOnceWithExactly(messages.SAVED_CONFIG(RC_FILE_NAME), config);
});
});
});
41 changes: 8 additions & 33 deletions test/unit/main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import esmock from 'esmock';
import { createSandbox } from 'sinon';
import sinonChai from 'sinon-chai';

import { RC_FILE_NAME } from '../../src/helpers/constants.js';
import { messages } from '../../src/helpers/messages.js';
import { globalMocksFactory } from '../helpers/global-mocks-factory.js';

use(sinonChai);
Expand All @@ -14,6 +12,8 @@ const sandbox = createSandbox();
class Logger {
static warn = sandbox.stub();
}
const configFileExists = sandbox.stub();
const getConfigFromFile = sandbox.stub();
const cloneRepository = sandbox.stub();
const findOasFromDir = sandbox.stub();
const findOasFromDirRecursive = sandbox.stub();
Expand All @@ -25,6 +25,7 @@ const startMockServer = sandbox.stub();

const mocks = {
'./helpers/logger.js': { Logger },
'./helpers/config-file.js': { configFileExists, getConfigFromFile },
'./services/user-flow-steps/init.js': { init },
'./services/user-flow-steps/init-with-schema-paths.js': { initWithSchemaPaths },
'./services/user-flow-steps/init-with-config-file.js': { initWithConfigFile },
Expand All @@ -34,7 +35,6 @@ const mocks = {
'./services/gitignore.js': { addToGitignore },
};
const globalMocks = globalMocksFactory(sandbox);
const { fs } = globalMocks;
const fileToTest = '../../src/main.js';
const absolutePath = new URL(fileToTest, import.meta.url).pathname;
const { main } = await esmock(absolutePath, absolutePath, mocks, globalMocks);
Expand All @@ -61,37 +61,25 @@ describe('unit: main', () => {

it('should init user flow and start the mock server using runConfig flag and config file does not exist', async () => {
program.opts.returns({ runConfig: true });
fs.existsSync.returns(false);
getConfigFromFile.returns(null);
init.resolves(expectedConfigMock);
await main();
expect(program.parse).to.have.been.calledOnce;
expect(program.opts).to.have.been.calledOnce;
expect(fs.existsSync).to.have.been.calledOnce;
expect(Logger.warn).to.have.been.calledWith(messages.CONFIG_FILE_NOT_FOUND, RC_FILE_NAME);
expect(init).to.have.been.calledOnceWithExactly();
expect(startMockServer).to.have.been.calledOnceWithExactly(expectedConfigMock.selectedSchemas);
});

it('should start the mock server using runConfig flag and config file exist', async () => {
program.opts.returns({ runConfig: true });
fs.existsSync.returns(true);
fs.readFileSync.returns(JSON.stringify(expectedConfigMock));
getConfigFromFile.returns(expectedConfigMock);
await main();
expect(program.parse).to.have.been.calledOnce;
expect(program.opts).to.have.been.calledOnce;
expect(fs.existsSync).to.have.been.calledOnce;
expect(init).to.have.not.been.called;
expect(startMockServer).to.have.been.calledOnceWithExactly(expectedConfigMock.selectedSchemas);
});

it('should init user flow using origin flag and start the mock server', async () => {
program.opts.returns({ origin });
fs.existsSync.returns(true);
fs.readFileSync.returns(JSON.stringify(expectedConfigMock));
init.resolves(expectedConfigMock);
await main();
expect(program.parse).to.have.been.calledOnce;
expect(program.opts).to.have.been.calledOnce;
expect(fs.existsSync).to.have.been.calledOnce;
expect(init).to.have.been.calledOnceWithExactly({
origin,
ports: undefined,
Expand All @@ -102,13 +90,8 @@ describe('unit: main', () => {

it('should init user flow using schema flag and start the mock server', async () => {
program.opts.returns({ schema });
fs.existsSync.returns(true);
fs.readFileSync.returns(JSON.stringify(expectedConfigMock));
initWithSchemaPaths.resolves(expectedConfigMock);
await main();
expect(program.parse).to.have.been.calledOnce;
expect(program.opts).to.have.been.calledOnce;
expect(fs.existsSync).to.have.been.calledOnce;
expect(initWithSchemaPaths).to.have.been.calledOnceWithExactly({
schemaPaths: schema,
ports: undefined,
Expand All @@ -118,26 +101,18 @@ describe('unit: main', () => {

it('should init user flow using config file only', async () => {
program.opts.returns({});
fs.existsSync.returns(true);
fs.readFileSync.returns(JSON.stringify(expectedConfigMock));
configFileExists.returns(true);
initWithConfigFile.resolves(expectedConfigMock);
await main();
expect(program.parse).to.have.been.calledOnce;
expect(program.opts).to.have.been.calledOnce;
expect(fs.existsSync).to.have.been.calledOnce;
expect(initWithConfigFile).to.have.been.calledOnceWithExactly();
expect(startMockServer).to.have.been.calledOnceWithExactly(expectedConfigMock.selectedSchemas);
});

it('should init user flow on no config file and no flags', async () => {
program.opts.returns({});
fs.existsSync.returns(false);
fs.readFileSync.returns(JSON.stringify(expectedConfigMock));
configFileExists.returns(false);
init.resolves(expectedConfigMock);
await main();
expect(program.parse).to.have.been.calledOnce;
expect(program.opts).to.have.been.calledOnce;
expect(fs.existsSync).to.have.been.calledOnce;
expect(init).to.have.been.calledOnceWithExactly();
expect(startMockServer).to.have.been.calledOnceWithExactly(expectedConfigMock.selectedSchemas);
});
Expand Down
11 changes: 5 additions & 6 deletions test/unit/services/user-flow-steps/helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { expect, use } from 'chai';
import esmock from 'esmock';
import { URL } from 'node:url';
import { createSandbox, match } from 'sinon';
import { createSandbox } from 'sinon';
import sinonChai from 'sinon-chai';

import { RC_FILE_NAME, TEMP_FOLDER_NAME } from '../../../../src/helpers/constants.js';
import { TEMP_FOLDER_NAME } from '../../../../src/helpers/constants.js';
import { messages } from '../../../../src/helpers/messages.js';
import { globalMocksFactory } from '../../../helpers/global-mocks-factory.js';

Expand All @@ -16,6 +16,7 @@ class Logger {
static info = sandbox.stub();
}
const verifyRemoteOrigin = sandbox.stub();
const saveConfigToFile = sandbox.stub();
const cloneRepository = sandbox.stub();
const findOasFromDir = sandbox.stub();
const findOasFromDirRecursive = sandbox.stub();
Expand All @@ -27,6 +28,7 @@ const mocks = {
'@inquirer/input': input,
'../../helpers/logger.js': { Logger },
'../../helpers/verify-remote-origin.js': { verifyRemoteOrigin },
'../../helpers/config-file.js': { saveConfigToFile },
'../clone-git-repository.js': { cloneRepository },
'../find-oas-from-dir.js': { findOasFromDir, findOasFromDirRecursive },
'../gitignore.js': { addToGitignore },
Expand Down Expand Up @@ -167,11 +169,8 @@ describe('unit: user-flow-steps', () => {
describe('saveRuntimeConfig', () => {
it('should save the config', async () => {
const config = { test: 'test' };
const rcFilePath = `path/to/${RC_FILE_NAME}`;
path.join.returns(rcFilePath);
await saveRuntimeConfig(config);
expect(globalMocks.fs.writeFileSync).to.have.been.calledWithMatch(rcFilePath, match.string);
expect(Logger.info).to.have.been.calledWithMatch(messages.SAVED_CONFIG(RC_FILE_NAME), config);
expect(saveConfigToFile).to.have.been.calledWithMatch(config);
expect(addToGitignore).to.have.been.called;
});
});
Expand Down
Loading

0 comments on commit 2e27d0b

Please # to comment.