Initial Commit.

This commit is contained in:
Sergio Díaz 2016-12-02 19:09:59 -06:00
commit 40f17dccaf
16 changed files with 2838 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

1
README.md Normal file
View File

@ -0,0 +1 @@
# Sleepy Cat's Backend

24
__tests__/beau.spec.js Normal file
View File

@ -0,0 +1,24 @@
const fs = require('fs');
const Beau = require('../beau');
const yaml = require('js-yaml');
describe(`Beau's config Loader.`, () => {
it ('Should only load valid configuration keys', () => {
let HOST = 'http:/martianwabbit.com';
let VERSION = 2;
let CACHE = false;
let shouldntBeAdded = true;
let beau = new Beau({
VERSION,
HOST,
CACHE,
shouldntBeAdded
});
expect(beau.config.HOST).toBe(HOST);
expect(beau.config.CACHE).toBe(CACHE);
expect(beau.config.VERSION).toBe(VERSION);
expect(beau.config.shouldntBeAdded).toBeUndefined();
});
});

45
__tests__/request.spec.js Normal file
View File

@ -0,0 +1,45 @@
const Request = require('../request');
const RequestCache = require('../requestCache');
const RequestList = require('../requestList');
describe('Request', () => {
let req;
let cache;
let request;
beforeEach(() => {
req = {
request: 'POST /user',
HOST: 'http://martianwabbit.com',
PARAMS: {
userId: '$profile.UserId'
},
HEADERS: {
authentication: 'BEARER $session.token'
},
PAYLOAD: {
username: 'seich'
}
};
cache = new RequestCache();
cache.add('$session', { token: 'abc123' });
cache.add('$profile', { UserId: 14 });
request = new Request(req);
});
test('It should load up the given request', () => {
expect(request.$verb).toBe('POST');
expect(request.$endpoint).toBe(req.HOST + '/user');
expect(request.$headers).toBeDefined();
expect(request.$payload).toBeDefined();
expect(request.$params).toBeDefined();
});
test('It should list all of its dependencies', () => {
expect(request.$dependencies.size).toBe(2);
expect(request.$dependencies).toContain('$session');
expect(request.$dependencies).toContain('$profile');
});
});

View File

@ -0,0 +1,73 @@
const RequestCache = require('../requestCache');
describe('Request Cache', () => {
let cache;
beforeEach(() => {
cache = new RequestCache();
cache.add('$session', {
hello: 'World'
});
cache.add('$array', [{
id: 1,
name: 'Sergio'
}, {
id: 2,
name: 'Angela'
}]);
});
it('should add keys to the cache', () => {
expect(cache.$cache.$session.hello).toBe('World');
});
describe('get', () => {
it('should be able to find key values with a given path', () => {
expect(cache.get('$session.hello')).toBe('World');
});
it('should throw when given an invalid path', () => {
expect(cache.get('$session.hello')).toThrow();
});
});
describe('parse', () => {
it('should transform variables in strings using it\'s cache', () => {
expect(cache.parse('Hello $session.hello')).toBe('Hello World');
});
it('should go transform variables in all values when given an object', () => {
let parsed = cache.parse({ hello: 'hello $session.hello', earth: '$session.hello' });
expect(parsed.hello).toBe('hello World');
expect(parsed.earth).toBe('World');
});
it('should return every non-string value as-is', () => {
let parsed = cache.parse({ number: 1, nulled: null, truthy: false, hello: '$session.hello' });
expect(parsed.number).toBe(1);
expect(parsed.nulled).toBeNull();
expect(parsed.truthy).toBe(false);
expect(parsed.hello).toBe('World');
});
it('should parse arrays as well', () => {
let parsed = cache.parse({ hello: '$array.0.name' })
expect(parsed.hello).toBe('Sergio');
console.log([parsed]);
});
});
describe('safely', () => {
it('should return an object when given an undefined value', () => {
expect(Object.keys(cache.safely(undefined)).length).toBe(0)
});
it('should parse any value other than undefined', () => {
expect(cache.safely('Hello $session.hello')).toBe('Hello World');
});
});
});

View File

@ -0,0 +1,26 @@
const RequestList = require('../requestList');
describe('RequestList', () => {
let host = 'http://martianwabbit.com';
let doc = {
'POST /session': null,
'Not a Request': null,
'POST /user': {
ALIAS: '$user',
PAYLOAD: {
name: 'Sergio',
lastname: 'Diaz'
}
}
};
it('should load valid requests', () => {
let requests = new RequestList(doc, { HOST: host });
let request = requests.list[0];
expect(requests.list.length).toBe(2);
expect(request.$verb).toBe('POST');
expect(request.$endpoint).toBe(host + '/session');
});
});

28
beau.js Normal file
View File

@ -0,0 +1,28 @@
const RequestList = require('./requestList');
class Beau {
constructor(doc) {
this.defaults = {
VERSION: 1,
CACHE: false,
HOST: ''
};
this.configKeys = Object.keys(this.defaults);
this.config = this.loadConfig(doc, this.defaults);
this.requests = new RequestList(doc, this.config);
}
loadConfig(doc, defaults = {}) {
var result = defaults;
Object.keys(doc)
.filter(k => this.configKeys.indexOf(k.toUpperCase()) > -1)
.forEach(k => result[k.toUpperCase()] = doc[k]);
return result;
}
}
module.exports = Beau;

94
bin/beau Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env node
const program = require('commander');
const Beau = require('../beau');
const yaml = require('js-yaml');
const fs = require('fs');
const { Line, Spinner } = require('clui');
const clc = require('cli-color');
const eyes = require('eyes');
program
.version('0.0.1')
.usage(`[options] -r <Request Alias>`)
.option('-r, --request [request]', `The alias for the request you'd like to trigger.`)
.option('-v, --verbose', `Show all the information related to the current request and it's response.`)
.option('-c, --config [file]', 'Specify your request config file. Defaults to beau.yml in the current directory.', 'beau.yml')
.option('-l, --list', `List all requests in the config file.`)
.option('-t, --truncate [length]', `Truncate the content to the given length`)
.parse(process.argv);
if (!fs.existsSync(program.config)) {
console.error(`The config file, ${program.config} was not found.`);
process.exit(1);
}
const config = yaml.safeLoad(fs.readFileSync(program.config, 'utf-8'));
const beau = new Beau(config);
if (typeof program.list === 'undefined' &&
typeof program.request === 'undefined') {
}
if (program.list) {
new Line().padding(2)
.column('HTTP Verb', 20, [clc.cyan])
.column('Alias', 30, [clc.cyan])
.column('Endpoint', 20, [clc.cyan])
.output();
beau.requests.list.forEach(({$verb, $alias, $endpoint}) =>
new Line().padding(2)
.column($verb, 20, [clc.yellow])
.column($alias, 30, [clc.yellow])
.column($endpoint)
.output()
);
new Line().output();
}
if (program.request) {
const request = `$${program.request}`;
const spinner = new Spinner(clc.yellow(`Requesting: ${request}`));
spinner.start();
beau.requests
.execByAlias(request)
.then(res => {
spinner.stop();
let { status, body } = res.response;
let { endpoint } = res.request;
status = status.toString().startsWith(2) ? clc.green(status) : clc.red(status);
new Line().padding(2)
.column('Status', 20, [clc.cyan])
.column('Endpoint', 20, [clc.cyan])
.output();
new Line().padding(2)
.column(status, 20)
.column(endpoint)
.output();
new Line().output();
let maxLength = +(program.truncate || res.length);
let inspect = eyes.inspector({ maxLength });
if (program.verbose) {
inspect(res);
} else {
inspect(body);
}
process.exit(0);
}).catch(function(err) {
console.error(err);
process.exit(1);
});
}

15
examples/github.yml Normal file
View File

@ -0,0 +1,15 @@
VERSION: 1
HOST: https://api.github.com
auth: &auth
HEADERS:
Authorization: token asfdasf123423sd1fgnh7d83n478
User-Agent: Beau
GET /user:
ALIAS: $user
<<: *auth
GET /user/repos:
ALIAS: $repos
<<: *auth

37
examples/slack.yml Normal file
View File

@ -0,0 +1,37 @@
VERSION: '1'
HOST: https://slack.com/api
auth: &auth
token: xoxp-139455775026-139455775090-140860933030-239230833dba65fc90078876ae85d9fb
GET /users.getPresence:
ALIAS: $presence
PARAMS:
<<: *auth
GET /channels.list:
ALIAS: $channel-list
PARAMS:
<<: *auth
exclude_archived: true
GET /channels.info:
ALIAS: $channel-info
PARAMS:
<<: *auth
channel: $channel-list.response.body.channels.0.id
POST /chat.postMessage:
ALIAS: $new-message
PARAMS:
<<: *auth
channel: $channel-info.response.body.channel.id
text: 'Hey Seich!'
parse: full
link_names: true
username: Beau
GET /users.list:
ALIAS: $user-list
PARAMS:
<<: *auth

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "beau",
"version": "0.0.1",
"description": "A tool for testing RESTful APIs",
"main": "beau.js",
"author": "Sergio Diaz <seich@martianwabbit.com>",
"license": "MIT",
"scripts": {
"test": "jest"
},
"dependencies": {
"cli-color": "^1.1.0",
"clui": "^0.3.1",
"commander": "^2.9.0",
"eyes": "^0.1.8",
"js-yaml": "^3.7.0",
"unirest": "^0.5.1"
},
"repository": "git@github.com:Seich/Beau.git",
"devDependencies": {
"jest": "^17.0.3"
},
"jest": {
"testEnvironment": "node",
"notify": false
},
"bin": {
"beau": "./bin/beau"
}
}

93
request.js Normal file
View File

@ -0,0 +1,93 @@
const unirest = require('unirest');
const {httpVerbs, requestRegex, replacementRegex} = require('./shared');
const RequestList = require('./requestList');
const RequestCache = require('./requestCache');
class Request {
constructor(req) {
let { request, ALIAS, PAYLOAD, HOST, PARAMS, HEADERS } = req;
let { verb, endpoint } = this.parseRequest(request);
this.$verb = verb;
this.$endpoint = HOST + endpoint;
this.$headers = HEADERS;
this.$payload = PAYLOAD;
this.$params = PARAMS;
this.$alias = ALIAS;
this.$dependencies = this.findDependencies(req);
}
parseRequest(request) {
let parts = request.match(requestRegex);
return {
verb: parts[1],
endpoint: parts[2]
};
}
findDependencies(request, set = new Set()) {
if (typeof request === 'object') {
Object.keys(request).forEach(key => {
if (key === 'ALIAS' || key.startsWith('$'))
return;
set = this.findDependencies(request[key], set);
});
} else if (typeof request === 'string') {
let matches = request.match(replacementRegex) || [];
let deps = matches.map(m => m.split('.')[0]);
return new Set([...set, ...deps]);
}
return set;
}
exec(list = new RequestList(), cache = new RequestCache()) {
let dependencies = [];
if (this.$dependencies.size > 0) {
dependencies = Array.from(this.$dependencies).map(dep => {
return list.execByAlias(dep);
});
}
return Promise.all(dependencies).then(() => {
let endpoint = cache.parse(this.$endpoint);
let request = unirest(this.$verb, endpoint);
request.headers(cache.safely(this.$headers));
request.query(cache.safely(this.$params));
request.send(cache.safely(this.$payload));
return new Promise((resolve, reject) => {
request.end(res => {
let results = {
request: {
headers: res.request.headers,
body: res.request.body,
endpoint: endpoint
},
response: {
status: res.status,
headers: res.headers,
body: res.body,
}
};
if (typeof this.$alias !== 'undefined') {
cache.add(this.$alias, results);
}
resolve(results);
});
});
});
}
}
module.exports = Request;

46
requestCache.js Normal file
View File

@ -0,0 +1,46 @@
const { replacementRegex } = require('./shared');
class RequestCache {
constructor() {
this.$cache = {};
}
add(key, value) {
this.$cache[key] = value;
}
get(path) {
let result = this.$cache;
path.split('.').forEach(part => {
if (result === undefined) return;
result = result[part];
});
if (typeof result === 'undefined') {
throw new Error('Key not found in cache: ', path);
}
return result;
}
safely(val) {
if (typeof val === 'undefined')
return {};
return this.parse(val);
}
parse(item) {
if (typeof item === 'string') {
return item.replace(replacementRegex, key => this.get(key));
} else if(typeof item === 'object' && item !== null) {
Object.keys(item).forEach(k => item[k] = this.parse(item[k]));
return item;
} else {
return item;
}
}
}
module.exports = RequestCache;

43
requestList.js Normal file
View File

@ -0,0 +1,43 @@
const Request = require('./request');
const RequestCache = require('./requestCache');
const httpVerbs = require('./shared').httpVerbs;
class RequestList {
constructor(doc = {}, config = {}) {
this.config = config;
this.list = this.loadRequests(doc);
this.cache = new RequestCache();
}
exec(request) {
return request.exec(this, this.cache);
}
execByAlias(alias) {
let request = this.list.find(r => r.$alias === alias);
if (typeof request === 'undefined') {
return Promise.reject(`${alias} not found among the requests.`);
}
return this.exec(request);
}
loadRequests(doc) {
let requestKeys = Object.keys(doc)
.filter(key => {
let verb = key.split(' ')[0].toUpperCase();
return httpVerbs.indexOf(verb) > -1;
});
return requestKeys.map(key => {
doc[key] = doc[key] || {};
doc[key].HOST = this.config.HOST;
doc[key].request = key;
return new Request(doc[key]);
});
}
}
module.exports = RequestList;

9
shared.js Normal file
View File

@ -0,0 +1,9 @@
const httpVerbs = ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'];
const requestRegex = new RegExp(`(${httpVerbs.join('|')})\\s(.*)`, 'i');
const replacementRegex = /(\$[a-zA-Z\.\d\-\_]*)/g;
module.exports = {
httpVerbs,
requestRegex,
replacementRegex
};

2273
yarn.lock Normal file

File diff suppressed because it is too large Load Diff