Added tests to all CLI commands. (#28)

* Moved the spinner initialization to the base class.

* Got rid of the base class, it complicated testing.

Now it lives on as a utils file. Should make it easier to test the CLI.

* Added a spec file for the ListCommand.

* Added tests for all CLI commands.

* Update some old tests.

Added missing cases and tests.

Most of these are kind useless but I hope I won't have to touch them
again.
This commit is contained in:
Sergio Díaz 2018-05-22 21:42:52 -06:00 committed by GitHub
parent 23064040df
commit 26b33fbf00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 519 additions and 89 deletions

14
bin/cli/__mocks__/fs.js Normal file
View File

@ -0,0 +1,14 @@
const fs = jest.genMockFromModule('fs');
fs.existsSync = filename => filename === 'beau.yml';
fs.readFileSync = () => `
version: 1
endpoint: https://example.org/
GET /anything:
alias: anything
payload:
name: $env.params.name
`;
module.exports = fs;

View File

@ -0,0 +1,41 @@
const Beau = require('../../../src/beau');
const original = require.requireActual('../utils');
const utils = {};
const config = {
environment: {
params: {
name: 'David'
}
},
endpoint: 'https://example.org',
version: 1,
'GET /anything': {
alias: 'anything',
payload: {
name: '$env.params.name'
}
},
'GET /status/418': {
alias: 'teapot'
}
};
utils.loadConfig = function() {
return new Beau(config, {});
};
utils.openConfigFile = function(filename) {
if (filename === 'beau.yml') {
return config;
}
if (filename === 'invalid-conf.yml') {
return { plugins: [{ hello: 1, world: 2 }] };
}
};
utils.baseFlags = original.baseFlags;
module.exports = utils;

View File

@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`List Command Should disable formatting when the flag is active. 1`] = `
Array [
"GET anything https://example.org/anything
",
"GET teapot https://example.org/status/418
",
]
`;
exports[`List Command Should list available requests for a given file. 1`] = `
Array [
" HTTP Verb Alias Endpoint
",
" GET anything https://example.org/anything
",
" GET teapot https://example.org/status/418
",
"
",
]
`;

View File

@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Request Command Should output an unformatted version 1`] = `
Array [
"200
",
"https://example.org/anything
",
"[]
",
"\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"
",
]
`;
exports[`Request Command Should output nothing 1`] = `Array []`;
exports[`Request Command Should output the response as json 1`] = `
Array [
"{\\"status\\":200,\\"headers\\":[],\\"body\\":\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"}
",
]
`;
exports[`Request Command Should output the response as json verboselly 1`] = `
Array [
"{\\"request\\":{\\"body\\":{\\"name\\":\\"David\\"},\\"endpoint\\":\\"https://example.org/anything\\"},\\"response\\":{\\"status\\":200,\\"headers\\":[],\\"body\\":\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"},\\"body\\":\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"}
",
]
`;
exports[`Request Command Should request the given alias 1`] = `
Array [
"",
" Status Endpoint
",
" 200 https://example.org/anything
",
"
",
"\\"{\\"hello\\": \\"world\\"}\\"
",
]
`;
exports[`Request Command Should show all information available when being verbose 1`] = `
Array [
"",
" Status Endpoint
",
" 200 https://example.org/anything
",
"
",
"{
request: {
body: {
name: \\"David\\"
},
endpoint: \\"https://example.org/anything\\"
},
response: {
status: 200,
headers: [],
body: \\"{\\"hello\\": \\"world\\"}\\"
},
body: \\"{\\"hello\\": \\"world\\"}\\"
}
",
]
`;

View File

@ -0,0 +1,90 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`utils loadConfig should load load the config onto Beau 1`] = `
Config {
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "https://example.org/",
"ENVIRONMENT": Object {
"_": Object {},
},
"HOSTS": Array [],
"PLUGINS": Plugins {
"context": Object {
"createReadStream": [Function],
},
"registry": Object {
"dynamicValues": Array [
Object {
"fn": [Function],
"name": "createReadStream",
},
],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
"REQUESTS": Array [
Object {
"ALIAS": "anything",
"COOKIEJAR": false,
"ENDPOINT": "https://example.org/",
"PAYLOAD": Object {
"name": "$env.params.name",
},
"REQUEST": "GET /anything",
},
],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
"COOKIEJAR",
],
"defaultConfigValues": Object {
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"doc": Object {
"GET /anything": Object {
"alias": "anything",
"payload": Object {
"name": "$env.params.name",
},
},
"endpoint": "https://example.org/",
"version": 1,
},
}
`;
exports[`utils loadConfig should load params onto the environment 1`] = `
Object {
"_": Object {
"BYE": "MARS",
"HELLO": "WORLD",
},
}
`;
exports[`utils openConfigFile should read and parse the given configuration file. 1`] = `
Object {
"GET /anything": Object {
"alias": "anything",
"payload": Object {
"name": "$env.params.name",
},
},
"endpoint": "https://example.org/",
"version": 1,
}
`;

View File

@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Validate Command should validate the configuration file 1`] = `
Array [
"beau.yml is valid.
",
]
`;

View File

@ -0,0 +1,28 @@
const ListCommand = require('../commands/list');
jest.mock('../utils');
describe('List Command', () => {
let result;
beforeEach(() => {
result = [];
jest
.spyOn(process.stdout, 'write')
.mockImplementation(val =>
result.push(require('strip-ansi')(val.toString('utf8')))
);
});
afterEach(() => jest.restoreAllMocks());
it('Should list available requests for a given file.', async () => {
await ListCommand.run([]);
expect(result).toMatchSnapshot();
});
it('Should disable formatting when the flag is active.', async () => {
await ListCommand.run(['--no-format']);
expect(result).toMatchSnapshot();
});
});

View File

@ -0,0 +1,55 @@
const RequestCommand = require('../commands/request');
const requestPromiseNativeMock = require('request-promise-native');
jest.mock('../utils');
describe('Request Command', () => {
let result;
beforeEach(() => {
requestPromiseNativeMock.fail = false;
result = [];
jest
.spyOn(process.stdout, 'write')
.mockImplementation(val =>
result.push(require('strip-ansi')(val.toString('utf8')))
);
});
afterEach(() => jest.restoreAllMocks());
it('Should request the given alias', async () => {
await RequestCommand.run(['anything']);
expect(result).toMatchSnapshot();
});
it('Should show all information available when being verbose', async () => {
await RequestCommand.run(['anything', '--verbose']);
expect(result).toMatchSnapshot();
});
it('Should output the response as json', async () => {
await RequestCommand.run(['anything', '--as-json']);
expect(result).toMatchSnapshot();
});
it('Should output the response as json verboselly', async () => {
await RequestCommand.run(['anything', '--as-json', '--verbose']);
expect(result).toMatchSnapshot();
});
it('Should output an unformatted version', async () => {
await RequestCommand.run(['anything', '--no-format']);
expect(result).toMatchSnapshot();
});
it('Should output nothing', async () => {
await RequestCommand.run(['anything', '--quiet']);
expect(result).toMatchSnapshot();
});
it('should thrown an error when the request fails', async () => {
requestPromiseNativeMock.fail = true;
await expect(RequestCommand.run(['anything'])).rejects.toThrow(Error);
});
});

View File

@ -0,0 +1,30 @@
const utils = require('../utils.js');
jest.mock('fs');
describe('utils', () => {
describe('openConfigFile', () => {
it('should read and parse the given configuration file.', () => {
expect(utils.openConfigFile('beau.yml')).toMatchSnapshot();
});
it('should thrown if given not given a file', () => {
expect(() => utils.openConfigFile('not-a-file.yml')).toThrow();
});
});
describe('loadConfig', () => {
it('should load load the config onto Beau', () => {
let beau = utils.loadConfig('beau.yml');
expect(beau.config).toMatchSnapshot();
});
it('should load params onto the environment', () => {
let beau = utils.loadConfig('beau.yml', [
'HELLO=WORLD',
'BYE=MARS'
]);
expect(beau.config.ENVIRONMENT).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,29 @@
const ValidateCommand = require('../commands/validate');
jest.mock('../utils');
describe('Validate Command', () => {
let result;
beforeEach(() => {
result = [];
jest
.spyOn(process.stdout, 'write')
.mockImplementation(val =>
result.push(require('strip-ansi')(val.toString('utf8')))
);
});
afterEach(() => jest.restoreAllMocks());
it('should validate the configuration file', async () => {
await ValidateCommand.run([]);
expect(result).toMatchSnapshot();
});
it('should show schema errors', async () => {
await expect(
ValidateCommand.run(['invalid-conf.yml'])
).rejects.toThrow();
});
});

View File

@ -1,51 +0,0 @@
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
const { Command, flags } = require('@oclif/command');
const Beau = require('../../src/beau');
class Base extends Command {
openConfigFile(configFile) {
if (!fs.existsSync(configFile)) {
this.error(`The config file, ${configFile} was not found.`);
this.exit(1);
}
return yaml.safeLoad(fs.readFileSync(configFile, 'utf-8'));
}
loadConfig(configFile, params = []) {
const config = this.openConfigFile(configFile);
const env = dotenv.config().parsed || {};
params = dotenv.parse(params.reduce((a, p) => a + '\n' + p, ''));
const envParams = { _: Object.assign(env, params) };
const configFileDir = path.dirname(
path.resolve(process.cwd(), configFile)
);
process.chdir(configFileDir);
return new Beau(config, envParams);
}
}
Base.flags = {
config: flags.string({
char: 'c',
description: 'The configuration file to be used.',
default: 'beau.yml'
}),
verbose: flags.boolean({
char: 'V',
description: `Show all additional information available for a command.`
}),
'no-format': flags.boolean({
description: `Disables color formatting for usage on external tools.`
})
};
module.exports = Base;

View File

@ -1,13 +1,12 @@
const clc = require('cli-color');
const { Line } = require('clui');
const { flags } = require('@oclif/command');
const { flags, Command } = require('@oclif/command');
const { baseFlags, loadConfig } = require('../utils');
const Base = require('../base');
class ListCommand extends Base {
class ListCommand extends Command {
async run() {
const { flags } = this.parse(ListCommand);
const Beau = this.loadConfig(flags.config);
const Beau = loadConfig(flags.config);
if (flags['no-format']) {
return Beau.requests.list.forEach(
@ -47,6 +46,6 @@ class ListCommand extends Base {
}
ListCommand.description = `Lists all available requests in the config file.`;
ListCommand.flags = { ...Base.flags };
ListCommand.flags = { ...baseFlags };
module.exports = ListCommand;

View File

@ -1,11 +1,10 @@
const clc = require('cli-color');
const jsome = require('jsome');
const { Line, Spinner } = require('clui');
const { flags } = require('@oclif/command');
const { flags, Command } = require('@oclif/command');
const { baseFlags, loadConfig } = require('../utils');
const Base = require('../base');
class RequestCommand extends Base {
class RequestCommand extends Command {
prettyOutput(res, verbose = false) {
let { status, body } = res.response;
@ -29,7 +28,7 @@ class RequestCommand extends Base {
new Line().output();
jsome((verbose ? res : body) || null);
this.log(jsome.getColoredString((verbose ? res : body) || null));
}
async run() {
@ -45,13 +44,10 @@ class RequestCommand extends Base {
args
} = this.parse(RequestCommand);
const Beau = this.loadConfig(config, params);
const Beau = loadConfig(config, params);
const spinnerSprite = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
this.spinner = new Spinner(
clc.yellow(`Requesting: ${args.alias}`),
spinnerSprite
);
this.spinner = new Spinner('', spinnerSprite);
let spinnerEnabled = !noFormat && !asJson && !quiet;
@ -78,7 +74,7 @@ class RequestCommand extends Base {
}
if (asJson) {
return this.log(JSON.stringify(res.response));
return this.log(JSON.stringify(verbose ? res : res.response));
}
if (noFormat) {
@ -95,7 +91,7 @@ class RequestCommand extends Base {
RequestCommand.description = `Executes a request by name.`;
RequestCommand.flags = {
...Base.flags,
...baseFlags,
param: flags.string({
char: 'P',
multiple: true,

View File

@ -1,17 +1,13 @@
const clc = require('cli-color');
const fs = require('fs');
const yaml = require('js-yaml');
const { flags } = require('@oclif/command');
const Base = require('../base');
const { flags, Command } = require('@oclif/command');
const { baseFlags, openConfigFile } = require('../utils');
const { validate } = require('../../../src/schema.js');
class ValidateCommand extends Base {
class ValidateCommand extends Command {
async run() {
const { flags, args } = this.parse(ValidateCommand);
const configFile = args.alias || flags.config;
const config = this.openConfigFile(configFile);
const config = openConfigFile(configFile);
let result = await validate(config);
if (result.valid) {
@ -23,7 +19,7 @@ class ValidateCommand extends Base {
}
ValidateCommand.description = `Validates the given configuration file against Beau's configuration schema.`;
ValidateCommand.flags = { ...Base.flags };
ValidateCommand.flags = { ...baseFlags };
ValidateCommand.args = [
{
name: 'alias',

49
bin/cli/utils.js Normal file
View File

@ -0,0 +1,49 @@
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
const Beau = require('../../src/beau');
const { flags } = require('@oclif/command');
const openConfigFile = configFile => {
if (!fs.existsSync(configFile)) {
throw new Error(`The config file, ${configFile} was not found.`);
}
return yaml.safeLoad(fs.readFileSync(configFile, 'utf-8'));
};
const loadConfig = (configFile, params = []) => {
const config = openConfigFile(configFile);
const env = dotenv.config().parsed || {};
params = dotenv.parse(params.reduce((a, p) => a + '\n' + p, ''));
const envParams = { _: Object.assign(env, params) };
const configFileDir = path.dirname(path.resolve(process.cwd(), configFile));
process.chdir(configFileDir);
return new Beau(config, envParams);
};
const baseFlags = {
config: flags.string({
char: 'c',
description: 'The configuration file to be used.',
default: 'beau.yml'
}),
verbose: flags.boolean({
char: 'V',
description: `Show all additional information available for a command.`
}),
'no-format': flags.boolean({
description: `Disables color formatting for usage on external tools.`
})
};
module.exports = {
openConfigFile,
loadConfig,
baseFlags
};

48
package-lock.json generated
View File

@ -1178,6 +1178,17 @@
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
},
"dependencies": {
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"ci-info": {
@ -6822,6 +6833,16 @@
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
},
"dependencies": {
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"string_decoder": {
@ -6839,11 +6860,20 @@
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
"ansi-regex": "^3.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
}
}
},
"strip-bom": {
@ -7593,6 +7623,16 @@
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
},
"dependencies": {
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"
}
}
}
},
"wrappy": {

View File

@ -7,7 +7,7 @@
"license": "MIT",
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
"test:coverage": "jest --coverage ./src"
},
"dependencies": {
"@oclif/command": "^1.4.16",
@ -29,7 +29,8 @@
},
"repository": "git@github.com:Seich/Beau.git",
"devDependencies": {
"jest": "^22.4.0"
"jest": "^22.4.0",
"strip-ansi": "^4.0.0"
},
"oclif": {
"commands": "./bin/cli/commands",

View File

@ -1,4 +1,3 @@
const yaml = require('js-yaml');
const Plugins = require('../plugins');
const Request = require('../request');
const RequestCache = require('../requestCache');

View File

@ -1,6 +1,5 @@
const Request = require('../request');
const RequestCache = require('../requestCache');
const RequestList = require('../requestList');
const requestPromiseNativeMock = require('request-promise-native');
describe('Request', () => {

View File

@ -0,0 +1,15 @@
const schema = require('../schema');
describe('Schema', () => {
it(`should validate an object against the schema`, async () => {
await expect(
schema.validate({ endpoint: 'http://example.com' })
).resolves.toHaveProperty('valid', true);
});
it(`should indicate the error when an schema is invalid`, async () => {
await expect(
schema.validate({ plugins: [{ hello: 1, world: 2 }] })
).resolves.toHaveProperty('valid', false);
});
});

View File

@ -62,9 +62,7 @@ const schema = Joi.object()
const validate = async function(config) {
try {
let results = await Joi.validate(config, schema, {
allowUnknown: true
});
await Joi.validate(config, schema, { allowUnknown: true });
return { valid: true };
} catch ({ name, details }) {
return {