Compare commits

..

No commits in common. "master" and "v0.7.3" have entirely different histories.

54 changed files with 10091 additions and 11950 deletions

1
.gitattributes vendored
View File

@ -1,2 +1 @@
* text=auto
*.js text eol=lf

View File

@ -1,37 +0,0 @@
name: Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- name: Install dependencies
run: |
npm install
wget https://codeclimate.com/downloads/test-reporter/test-reporter-0.6.3-linux-amd64 -O cc-test-reporter
chmod +x ./cc-test-reporter
- name: Validate Schema
run: |
npx ajv-cli compile -s schema.json
npx ajv-cli validate -s schema.json -d examples/beau.yml
- name: Run CLI Tests
run: npm test -- ./bin
- name: Run Lib Tests
run: npm run test:coverage
- name: Report Results
if: success()
run: |
./cc-test-reporter format-coverage
./cc-test-reporter upload-coverage
env:
GIT_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
GIT_BRANCH: ${{ github.head_ref }}
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE_REPO_TOKEN }}

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules/
coverage/
*.swp

View File

@ -1,10 +0,0 @@
printWidth: 80
tabWidth: 4
singleQuote: true
useTabs: false
trailingComma: none
bracketSpacing: true
jsxBracketSameLine: true
semi: false
requirePragma: false
proseWrap: always

42
LICENSE
View File

@ -1,41 +1,7 @@
“Commons Clause” License Condition v1.0
Copyright 2018 David Sergio Díaz
The Software is provided to you by the Licensor under the License, as defined
below, subject to the following condition.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Without limiting other conditions in the License, the grant of rights under the
License will not include, and the License does not grant to you, the right to
Sell the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the rights
granted to you under the License to provide to third parties, for a fee or other
consideration (including without limitation fees for hosting or consulting/
support services related to the Software), a product or service whose value
derives, entirely or substantially, from the functionality of the Software. Any
license notice or attribution required by the License must also include this
Commons Clause License Condition notice.
Software: Beau
License: MIT
Licensor: David Díaz
---
Copyright 2020 David Díaz
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,23 +1,22 @@
<div align="center">
<img src="media/beau.png" height="144" alt="Beau's Logo is a Seahorse" />
<img src="http://files.martianwabbit.com/beau.png?1" height="144"/>
</div>
<h1 align="center">Beau</h1>
<p align="center">Testing JSON APIs made easy.</p>
<p align="center">
<a href="https://codeclimate.com/github/Seich/Beau/maintainability"><img src="https://api.codeclimate.com/v1/badges/bc2de4d71893d6a2d18b/maintainability" /></a>
<a href="https://codeclimate.com/github/Seich/Beau/test_coverage"><img src="https://api.codeclimate.com/v1/badges/bc2de4d71893d6a2d18b/test_coverage" /></a>
<img src="https://github.com/Seich/Beau/workflows/Tests/badge.svg"/>
<a href="https://codeclimate.com/github/Seich/Beau/maintainability"><img src="https://api.codeclimate.com/v1/badges/bc2de4d71893d6a2d18b/maintainability" /></a>
<a href="https://codeclimate.com/github/Seich/Beau/test_coverage"><img src="https://api.codeclimate.com/v1/badges/bc2de4d71893d6a2d18b/test_coverage" /></a>
<a href="https://circleci.com/gh/Seich/Beau/tree/master"><img src="https://circleci.com/gh/Seich/Beau/tree/master.svg?style=svg" alt="CircleCI"></a>
</p>
## What is Beau?
Beau is a modern http client. It uses a YAML file as configuration allowing you
to test APIs without having to write lengthy commands.
Beau, is a CLI that executes HTTP requests based on a YAML configuration file.
This makes testing easy, it allows you to share test requests with others as
part of your repo.
<div align="center">
<img src="media/usage.gif" alt="A gif showing how beau works." />
</div>
![A Gif showing how beau works](http://files.martianwabbit.com/beau2.gif)
## Installation
@ -25,45 +24,52 @@ to test APIs without having to write lengthy commands.
## Usage
$ beau [COMMAND]
⚡ beau --help
COMMANDS
help display help for beau
list Lists all available requests in the config file.
request Executes a request by name.
validate Validates the given configuration file against Beau's configuration schema.
Usage: beau [options] [command]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
request [options] <alias>
list [options]
## Example Configuration File
```yaml
endpoint: https://httpbin.org/
version: 1
endpoint: https://example.com/api/
POST /anything:
alias: anything
payload:
hello: world
```
POST /session:
ALIAS: session
PAYLOAD:
username: seich
password: hello01
```
$ beau request anything
GET /profile
ALIAS: profile
HEADERS:
authorization: Bearer $session.response.body.token
Status Endpoint
200 https://httpbin.org/anything
GET /user/$profile.response.body.id/posts
ALIAS: friends
HEADERS:
authorization: Bearer $session.response.body.token
PARAMS:
archived: true
{
...
json: {
hello: "world"
},
method: "POST",
url: "https://httpbin.org/anything"
...
}
```
## Example Usage
## Documentation
beau request profile
Visit https://beaujs.com/docs/ for the complete docs.
That would execute the profile request along with it´s dependencies. In this
case, the session request would be made as well since we are using it´s response
value as part of our current request.
## License

189
bin/beau
View File

@ -1,5 +1,188 @@
#!/usr/bin/env node
const program = require('commander');
const process = require('process');
const Beau = require('../src/beau');
const yaml = require('js-yaml');
const fs = require('fs');
const { Line, Spinner } = require('clui');
const clc = require('cli-color');
const jsome = require('jsome');
const dotenv = require('dotenv');
const updateNotifier = require('update-notifier');
require('@oclif/command')
.run()
.catch(require('@oclif/errors/handle'));
const package = require('../package.json');
updateNotifier({ pkg: package }).notify();
program.version(package.version);
program
.command('request <alias>')
.option(
'-c --config <config>',
'Specify your request config file. Defaults to beau.yml in the current directory.',
'beau.yml'
)
.option(
'--verbose',
'Show all the information available on the current request.',
false
)
.option('--no-format', 'Return the text without any special formatting.')
.action(async (alias, { config, format, verbose }) => {
const beau = loadConfig(config);
let spinner;
if (format) {
spinner = new Spinner(clc.yellow(`Requesting: ${alias}`));
spinner.start();
}
try {
let res = await beau.requests.execByAlias(alias);
let { status, headers, body } = res.response;
let { endpoint } = res.request;
if (format) {
spinner.stop();
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();
if (verbose) {
jsome(res);
} else {
jsome(body);
}
} else {
console.log(status);
console.log(endpoint);
console.log(JSON.stringify(headers));
console.log(JSON.stringify(body));
}
process.exit(0);
} catch (err) {
new Line().output();
console.error(err.message);
process.exit(1);
}
});
program
.command('list')
.option(
'-c --config <config>',
'Specify your request config file. Defaults to beau.yml in the current directory.',
'beau.yml'
)
.option('--no-format', 'Return the text without any special formatting.')
.action(({ config, format }) => {
const beau = loadConfig(config);
if (format) {
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();
} else {
beau.requests.list.forEach(({ VERB, ALIAS, ENDPOINT }) => {
console.log(`${VERB}\t${ALIAS}\t${ENDPOINT}`);
});
}
});
program
.command('init')
.option(
'-e --endpoint <endpoint>',
'Allows you to set the default endpoint',
null
)
.action(({ endpoint }) => {
const newFile = `# Beau.yml
version: 1${
endpoint === null
? `
# endpoint: http://example.com
`
: `
endpoint: ${endpoint}
`
}
# defaults:
# params:
# userId: 25
# GET /profile: profile
# GET /posts:
# alias: posts
# params:
# order: ASC
# POST /profile:
# alias: save-profile
# headers:
# authentication: Bearer token
# payload:
# name: David
# lastname: Diaz
`;
if (!fs.existsSync('beau.yml')) {
fs.writeFileSync('beau.yml', newFile);
console.info('beau.yml created!');
} else {
console.error('beau.yml already exists.');
}
});
program.parse(process.argv);
if (!program.args.length) {
program.help();
}
function loadConfig(configFile) {
if (!fs.existsSync(configFile)) {
console.error(`The config file, ${configFile} was not found.`);
process.exit(1);
}
const config = yaml.safeLoad(fs.readFileSync(configFile, 'utf-8'));
const env = dotenv.config().parsed || {};
return new Beau(config, env);
}

View File

@ -1,30 +0,0 @@
const Beau = require('../../../src/beau')
const original = jest.requireActual('../base')
const config = {
environment: {
params: {
name: 'David'
}
},
endpoint: 'https://example.org',
version: 1,
'GET /anything': {
alias: 'alias',
payload: {
name: '$env.params.name'
}
},
'GET /status/418': {
alias: 'teapot'
}
}
class Base extends original {
loadConfig(configFile, params = []) {
return new Beau(config, {})
}
}
module.exports = Base

View File

@ -1,14 +0,0 @@
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

@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`List Command with flags: 1`] = `
Array [
" HTTP Verb Alias Endpoint
",
" GET alias https://example.org/anything
",
" GET teapot https://example.org/status/418
",
"
",
]
`;
exports[`List Command with flags: 2`] = `
Array [
"GET alias https://example.org/anything
",
"GET teapot https://example.org/status/418
",
]
`;

View File

@ -1,71 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Request Command with flags: alias %s %s 1`] = `
Array [
"",
" Status Endpoint
",
" 200 https://example.org/anything
",
"
",
"{\\"hello\\": \\"world\\"}
",
]
`;
exports[`Request Command with flags: alias --as-json %s 1`] = `
Array [
"{\\"status\\":200,\\"headers\\":[],\\"body\\":\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"}
",
]
`;
exports[`Request Command with flags: alias --as-json --verbose 1`] = `
Array [
"{\\"request\\":{\\"body\\":{\\"name\\":\\"David\\"},\\"endpoint\\":\\"https://example.org/anything\\"},\\"response\\":{\\"status\\":200,\\"headers\\":[],\\"body\\":\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"},\\"body\\":\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"}
",
]
`;
exports[`Request Command with flags: alias --no-format %s 1`] = `
Array [
"200
",
"https://example.org/anything
",
"[]
",
"\\"{\\\\\\"hello\\\\\\": \\\\\\"world\\\\\\"}\\"
",
]
`;
exports[`Request Command with flags: alias --quiet %s 1`] = `Array []`;
exports[`Request Command with flags: alias --verbose %s 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

@ -1,23 +0,0 @@
const ListCommand = require('../commands/list')
jest.mock('../../../src/shared')
jest.mock('../base')
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())
test.each([[], ['--no-format']])('with flags:', async (...args) => {
await ListCommand.run(args)
expect(result).toMatchSnapshot()
})
})

View File

@ -1,37 +0,0 @@
const RequestCommand = require('../commands/request')
const requestPromiseNativeMock = require('request-promise-native')
jest.mock('../../../src/shared')
jest.mock('../base')
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())
test.each([
['alias'],
['alias', '--verbose'],
['alias', '--as-json'],
['alias', '--as-json', '--verbose'],
['alias', '--no-format'],
['alias', '--quiet']
])('with flags: %s %s %s', async (...args) => {
await RequestCommand.run(args)
expect(result).toMatchSnapshot()
})
it('should throw an error when the request fails', async () => {
requestPromiseNativeMock.fail = true
await expect(RequestCommand.run(['anything'])).rejects.toThrow(Error)
})
})

View File

@ -1,77 +0,0 @@
const { Command, flags } = require('@oclif/command')
const yaml = require('js-yaml')
const fs = require('fs')
const path = require('path')
const dotenv = require('dotenv')
const Beau = require('../../src/beau')
const Ajv = require('ajv').default
const betterAjvErrors = require('better-ajv-errors')
const schema = require('../../schema.json')
const ajv = new Ajv()
const validate = ajv.compile(schema)
class Base extends Command {
openConfigFile(configFile) {
if (!fs.existsSync(configFile)) {
throw new Error(`The config file, ${configFile} was not found.`)
}
let config
yaml.loadAll(fs.readFileSync(configFile, 'utf-8'), (doc) => {
const valid = validate(doc)
if (!valid) {
this.log(`The configuration file is not valid.`)
this.error(
betterAjvErrors(schema, doc, validate.errors, { indent: 2 })
)
}
if (typeof config === 'undefined') {
config = doc
} else {
if (typeof config.hosts === 'undefined') {
config.hosts = []
}
config.hosts.push(doc)
}
})
return config
}
loadConfig(configFile, params = []) {
const config = this.openConfigFile(configFile)
const env = dotenv.config().parsed || {}
params = dotenv.parse(params.join('\n'))
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,46 +0,0 @@
const clc = require('cli-color')
const { Line } = require('clui')
const { expandPath } = require('../../../src/shared')
const Base = require('../base')
class ListCommand extends Base {
async run() {
const { flags } = this.parse(ListCommand)
const Beau = this.loadConfig(flags.config)
if (flags['no-format']) {
return Beau.requests.list.forEach(
({ VERB, ALIAS, ENDPOINT, PATH }) =>
this.log(
`${VERB}\t${ALIAS}\t${ENDPOINT.replace(
/\/$/,
''
)}/${PATH.replace(/^\//, '')}`
)
)
}
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, PATH }) =>
new Line()
.padding(2)
.column(VERB, 20, [clc.yellow])
.column(ALIAS, 30, [clc.yellow])
.column(expandPath(ENDPOINT, PATH))
.output()
)
new Line().output()
}
}
ListCommand.description = `Lists all available requests in the config file.`
ListCommand.flags = { ...Base.flags }
module.exports = ListCommand

View File

@ -1,158 +0,0 @@
const Base = require('../base')
const cj = require('color-json')
const clc = require('cli-color')
const prompts = require('prompts')
const { Line, Spinner } = require('clui')
const { flags } = require('@oclif/command')
const { expandPath } = require('../../../src/shared')
class RequestCommand extends Base {
prettyOutput(res, verbose = false) {
let { status, body } = res.response
this.spinner.stop()
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(res.request.endpoint)
.output()
new Line().output()
const result = (verbose ? res : body) || null
if (typeof result === 'object') {
this.log(cj(result))
} else if (typeof result === 'string') {
this.log(result)
}
}
async run() {
const {
flags: {
param: params,
config,
'no-format': noFormat = false,
verbose = false,
'as-json': asJson = false,
quiet = false,
interactive = false
},
args
} = this.parse(RequestCommand)
const Beau = this.loadConfig(config, params)
const spinnerSprite = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']
this.spinner = new Spinner('', spinnerSprite)
let spinnerEnabled = !noFormat && !asJson && !quiet
if (typeof args.alias == 'undefined' && !interactive) {
this.error(
'Missing 1 required argument: The alias of the request to execute.'
)
}
if (interactive) {
const requests = Beau.requests.list.map(
({ VERB, ALIAS, ENDPOINT, PATH }) => ({
title: `${VERB} ${PATH} - ${ALIAS}`,
value: ALIAS,
description: expandPath(ENDPOINT, PATH)
})
)
const { name } = await prompts({
name: 'name',
message: 'Pick a Request to execute',
type: 'select',
choices: requests
})
args.alias = name
}
if (spinnerEnabled) {
this.spinner.start()
}
let res
try {
res = await Beau.requests.execByAlias(args.alias)
} catch (err) {
this.spinner.stop()
if (!quiet) {
this.error(err.message)
}
this.exit(1)
}
if (quiet) {
return
}
if (asJson) {
return this.log(JSON.stringify(verbose ? res : res.response))
}
if (noFormat) {
this.log(res.response.status)
this.log(res.request.endpoint)
this.log(JSON.stringify(res.response.headers))
this.log(JSON.stringify(res.response.body))
return
}
this.prettyOutput(res, verbose)
}
}
RequestCommand.description = `Executes a request by name.`
RequestCommand.flags = {
...Base.flags,
param: flags.string({
char: 'P',
multiple: true,
default: [],
description: `Allows you to inject values into the request's environment.`
}),
quiet: flags.boolean({
description: `Skips the output.`
}),
'as-json': flags.boolean({
char: 'j',
description: `Outputs the response as json.`
}),
interactive: flags.boolean({
char: 'i',
description: 'Choose request interactively.',
default: false
})
}
RequestCommand.args = [
{
name: 'alias',
required: false,
description: `The alias of the request to execute.`
}
]
module.exports = RequestCommand

12
circle.yml Normal file
View File

@ -0,0 +1,12 @@
machine:
node:
version: 8.9.3
dependencies:
post:
- npm install -g codeclimate-test-reporter
test:
pre:
- npm run test:coverage
- codeclimate-test-reporter < ./coverage/lcov.info

View File

@ -1,37 +0,0 @@
endpoint: https://pokeapi.co/api/v2/
# Try replacing this pokemon using params:
# beau request get-pokemon -P "pokemon=dragapult"
environment:
_:
pokemon: ditto
cookiejar: true
GET /pokemon/$env._.pokemon: get-pokemon
GET $get-pokemon.body.location_area_encounters: get-encounters
POST https://httpbin.org/anything:
- alias: post-first-area
payload:
area: $get-encounters.body.0.location_area.name
- alias: post-pokemon-type
payload:
type: $get-pokemon.body.types.0.type.name
- alias: form-submission
form:
name: Dragapult
- alias: file-upload
formdata:
name: Beau
logo: $[createReadStream('../media/beau.png')]
GET https://httpbin.org/status/418: teapot
GET https://httpbin.org/cookies/set:
alias: set-cookies
params:
hello: World

15
examples/github.yml Normal file
View File

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

32
examples/hosts.yml Normal file
View File

@ -0,0 +1,32 @@
version: 1
environment:
the:
post: 2
defaults:
headers:
hello: 'Hello2'
GET http://jsonplaceholder.typicode.com/posts/1:
alias: a-post
headers:
hello: $jpa2:get-post.body.id
hosts:
- host: jpa
endpoint: http://jsonplaceholder.typicode.com
GET /posts/$env.the.post: get-post
GET /users/$jpa:get-post.body.userId: hello
- host: jpa2
endpoint: http://jsonplaceholder.typicode.com
defaults:
headers: false
GET /posts/$jpa:get-post.body.id:
alias: get-post

View File

@ -0,0 +1,29 @@
version: 1
endpoint: 'http://jsonplaceholder.typicode.com'
environment:
the:
post: 1
defaults:
headers:
hello: $posts.body.0.userId
GET /posts/$env.the.post: get-post
GET /posts/:
alias: posts
headers:
hello: false
POST /posts/:
alias: new-post
documentation:
title: New Post
GET /users/$posts.body.0.userId:
alias: post-user
documentation:
description: Fetches the user for a give post.
params:
hello: 'world'

11
examples/plugins.yml Normal file
View File

@ -0,0 +1,11 @@
endpoint: http://localhost:10080
plugins:
- beau-jwt:
data:
userId: 12
name: Sergio
secret: 'asdfasdf+asdfasdf/asdfasdfasdfasdf=='
GET /test:
alias: test

37
examples/slack.yml Normal file
View File

@ -0,0 +1,37 @@
VERSION: '1'
ENDPOINT: https://slack.com/api
auth: &auth
token: xoxp-139455775026-139455775090-147751461120-f224ed6ffee029869a0f138d0859e7d6
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 616 KiB

15637
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +1,40 @@
{
"name": "beau",
"version": "0.11.3",
"description": "Testing APIs made easy.",
"main": "./src/beau.js",
"author": "David Díaz <seich@martianwabbit.com>",
"license": "MIT + Commons Clause",
"scripts": {
"test": "jest -i",
"test:coverage": "jest --coverage ./src",
"release": "np"
},
"files": [
"/src",
"/bin"
],
"dependencies": {
"@oclif/command": "1.8.0",
"@oclif/config": "1.17.0",
"@oclif/plugin-help": "3.2.1",
"@oclif/plugin-warn-if-update-available": "1.7.0",
"ajv": "7.0.3",
"beau-std": "0.9.4",
"better-ajv-errors": "0.7.0",
"cli-color": "2.0.0",
"clui": "0.3.6",
"color-json": "2.0.1",
"deepmerge": "4.2.2",
"dotenv": "8.2.0",
"globby": "11.0.2",
"is-plain-object": "5.0.0",
"js-yaml": "4.0.0",
"prompts": "2.4.0",
"request": "2.88.2",
"request-promise-native": "1.0.9",
"requireg": "0.2.2"
},
"repository": "git@github.com:Seich/Beau.git",
"devDependencies": {
"jest": "26.6.3",
"jest-watch-typeahead": "0.6.1",
"strip-ansi": "6.0.0",
"np": "7.2.0"
},
"oclif": {
"commands": "./bin/cli/commands",
"bin": "beau",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-warn-if-update-available"
]
},
"jest": {
"testEnvironment": "node",
"notify": true,
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
},
"bin": {
"beau": "./bin/beau"
},
"engines": {
"node": ">=8.10.0"
}
"name": "beau",
"version": "0.7.3",
"description": "A tool for testing JSON APIs",
"main": "./src/beau.js",
"author": "Sergio Diaz <seich@martianwabbit.com>",
"license": "MIT",
"scripts": {
"test": "jest",
"watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"dependencies": {
"cli-color": "^1.1.0",
"clui": "^0.3.1",
"commander": "^2.12.2",
"deepmerge": "^2.0.1",
"dotenv": "^5.0.1",
"js-yaml": "^3.7.0",
"jsome": "^2.3.26",
"request": "^2.83.0",
"request-promise-native": "^1.0.5",
"requireg": "^0.1.6",
"update-notifier": "^2.3.0"
},
"repository": "git@github.com:Seich/Beau.git",
"devDependencies": {
"jest": "22.0.4"
},
"jest": {
"testEnvironment": "node",
"notify": true
},
"bin": {
"beau": "./bin/beau"
},
"engines": {
"node": ">=8.9.3"
}
}

View File

@ -1,122 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://beaujs.com/schema.json",
"title": "Beaujs Requests Schema",
"type": "object",
"definitions": {
"request": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"$ref": "#/definitions/requestObject",
"required": ["alias"]
}
]
},
"requestObject": {
"type": "object",
"properties": {
"alias": {
"type": "string",
"description": "The name of this request."
},
"headers": {
"type": "object",
"additionalProperties": true,
"description": "Headers that are part of this request."
},
"params": {
"type": "object",
"additionalProperties": true,
"description": "Query String parameters to add to this request."
},
"payload": {
"description": "This request's body. It is converted to json automatically if given an object. It's sent as a string otherwise.",
"oneOf": [
{ "type": "string" },
{
"type": "object",
"additionalProperties": true
}
]
},
"form": {
"type": "object",
"additionalProperties": true,
"description": "This request's body. Sets the content-type to application/x-www-form-urlencoded."
},
"formdata": {
"type": "object",
"additionalProperties": true,
"description": "This request's body. Sets the content-type to multipart/form-data."
}
},
"allOf": [
{ "not": { "required": ["payload", "form"] } },
{ "not": { "required": ["payload", "formdata"] } },
{ "not": { "required": ["formdata", "form"] } }
]
}
},
"properties": {
"version": {
"type": "number",
"description": "The beau version this document was created for.",
"enum": [1]
},
"endpoint": {
"type": "string",
"description": "The root endpoint for this host."
},
"cookiejar": {
"type": "boolean",
"description": "Enable cookie support for requests?"
},
"host": {
"type": "string",
"description": "The name of the current host. It allows referencing requests between different hosts."
},
"defaults": {
"description": "Default values to be added to all requests.",
"$ref": "#/definitions/requestObject"
},
"plugins": {
"description": "Plugins to be enabled for this document.",
"type": "array",
"items": {
"anyOf": [
{ "type": "string" },
{ "type": "object", "additionalProperties": true }
]
}
},
"environment": {
"type": "object",
"description": "Global document variables for easy access.",
"additionalProperties": true,
"properties": {
"_": {
"type": "object",
"description": "Environment variables brought in by cli params or dotenv.",
"additionalProperties": true
}
}
}
},
"patternProperties": {
"(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH)\\s.*": {
"oneOf": [
{
"$ref": "#/definitions/request"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/request"
}
}
]
}
}
}

View File

@ -1,11 +0,0 @@
class DynamicValues {
constructor(registry, settings = {}) {
registry.defineDynamicValue('add', this.add)
}
add(x, y) {
return x + y
}
}
module.exports = DynamicValues

View File

@ -1,20 +0,0 @@
class Modifiers {
constructor(registry, settings = {}) {
registry.addPreRequestModifier(this.preRequest)
registry.addPostRequestModifier(this.postRequest)
}
preRequest(request, orig) {
request.headers = request.headers || {}
request.headers.preRequestModifier = true
return request
}
postRequest(response, orig) {
response.body = 'Hello World'
response.response.body = 'Hello World'
return response
}
}
module.exports = Modifiers

View File

@ -1,7 +0,0 @@
class BeauStd {
constructor(registry, settings) {
registry.defineDynamicValue('createReadStream', () => {})
}
}
module.exports = BeauStd

View File

@ -1,22 +1,22 @@
function Request(request) {
if (Request.fail) {
throw new Error()
}
if (Request.fail) {
throw new Error();
}
return {
request: {
headers: request.headers,
body: request.body,
uri: {
href: `${request.baseUrl}${request.uri}`
}
},
statusCode: 200,
headers: [],
body: '{"hello": "world"}'
}
return {
request: {
headers: request.headers,
body: request.body,
uri: {
href: request.url
}
},
statusCode: 200,
headers: [],
body: '{"hello": "world"}'
};
}
Request.fail = false
Request.fail = false;
module.exports = Request
module.exports = Request;

View File

@ -1,15 +1,19 @@
function requireg(name) {
return require(name)
}
module.exports = function(name) {
return function(settings) {
let response = {
name: `${name}`,
preRequest(request) {
request.wasModified = true;
requireg.resolving = true
return request;
},
requireg.resolve = function (name) {
if (requireg.resolving) {
return ''
} else {
return undefined
}
}
postResponse(response) {
response.changed = true;
return response;
}
};
module.exports = requireg
return response;
};
};

View File

@ -1,4 +0,0 @@
module.exports = {
...jest.requireActual('../shared'),
moduleVersion: jest.fn().mockReturnValue(1)
}

View File

@ -1,117 +1,207 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Beau's config Loader. should load the config 1`] = `
exports[`Beau's config Loader. should create a request list 1`] = `
Beau {
"config": Config {
"COOKIEJAR": false,
"CACHE": false,
"DEFAULTS": Object {
"headers": Object {
"authentication": "hello",
},
},
"ENDPOINT": "http://example.com",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Plugins {
"autoload": Array [
"std",
],
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
"REQUESTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
"COOKIEJAR",
],
"defaultConfigValues": Object {
"CACHE": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"doc": Object {
"GET /posts/1": "get-post",
"GET /user": Object {
"alias": "user",
"headers": Object {
"hello": "world",
},
},
"defaults": Object {
"headers": Object {
"authentication": "hello",
},
},
"endpoint": "http://jsonplaceholder.typicode.com",
"version": 1,
},
"requests": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
},
"requests": RequestList {
"cache": RequestCache {
"$cache": Object {
"env": Object {},
"$env": Object {},
},
},
"list": Array [],
},
}
`;
exports[`Beau's config Loader. should load the request list using the configuration 1`] = `
RequestList {
"cache": RequestCache {
"$cache": Object {
"env": Object {},
},
},
"list": Array [
Request {
"ALIAS": "get-post",
"COOKIEJAR": false,
"DEPENDENCIES": Set {},
"ENDPOINT": "http://example.com",
"PATH": "/posts/1",
"REQUEST": "GET /posts/1",
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"REQUEST": "GET /posts/1",
},
"plugins": Plugins {
"autoload": Array [
"std",
],
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
"config": Config {
"CACHE": false,
"DEFAULTS": Object {
"headers": Object {
"authentication": "hello",
},
},
},
Request {
"ALIAS": "user",
"COOKIEJAR": false,
"DEPENDENCIES": Set {},
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
],
"defaultConfigValues": Object {
"CACHE": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"PATH": "/user",
"REQUEST": "GET /user",
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "user",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"doc": Object {
"GET /posts/1": "get-post",
"GET /user": Object {
"alias": "user",
"headers": Object {
"hello": "world",
},
},
"defaults": Object {
"headers": Object {
"authentication": "hello",
},
},
"endpoint": "http://jsonplaceholder.typicode.com",
"version": 1,
},
"requests": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
},
"list": Array [
Request {
"ALIAS": "get-post",
"DEPENDENCIES": Set {},
"ENDPOINT": "http://jsonplaceholder.typicode.com/posts/1",
"HEADERS": Object {
"authentication": "hello",
},
"PARAMS": undefined,
"PAYLOAD": undefined,
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
},
Request {
"ALIAS": "user",
"DEPENDENCIES": Set {},
"ENDPOINT": "http://jsonplaceholder.typicode.com/user",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"PARAMS": undefined,
"PAYLOAD": undefined,
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
},
],
"modifiers": Array [],
"requests": Array [
Object {
"ALIAS": "get-post",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
"plugins": Plugins {
"autoload": Array [
"std",
],
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
},
],
],
},
}
`;

View File

@ -2,7 +2,7 @@
exports[`Config should load multiple hosts 1`] = `
Config {
"COOKIEJAR": false,
"CACHE": false,
"DEFAULTS": Object {
"HEADERS": Object {
"hello": "mars",
@ -41,21 +41,69 @@ Config {
"host": "info",
},
],
"PLUGINS": Plugins {
"autoload": Array [
"std",
],
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
"PLUGINS": Array [],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
],
"defaultConfigValues": Object {
"CACHE": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"REQUESTS": Array [
"doc": Object {
"GET /e1": "e1",
"defaults": Object {
"HEADERS": Object {
"hello": "mars",
},
},
"endpoint": "http://example.org",
"hosts": Array [
Object {
"GET /e2": "e2",
"GET /posts": "posts",
"defaults": Object {
"HEADERS": Object {
"hello": "world",
"world": "hello",
},
},
"endpoint": "http://example.com",
"host": "com",
},
Object {
"GET /e3": "e3",
"GET /posts": "posts",
"defaults": Object {
"HEADERS": Object {
"hello": "world",
"world": "bye",
},
},
"endpoint": "http://example.net",
"host": "net",
},
Object {
"GET /posts": "posts",
"endpoint": "http://example.info",
"host": "info",
},
],
},
"requests": Array [
Object {
"ALIAS": "e1",
"COOKIEJAR": false,
"ENDPOINT": "http://example.org",
"HEADERS": Object {
"hello": "mars",
@ -64,7 +112,6 @@ Config {
},
Object {
"ALIAS": "com:e2",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
@ -74,7 +121,6 @@ Config {
},
Object {
"ALIAS": "com:posts",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"HEADERS": Object {
"hello": "world",
@ -84,7 +130,6 @@ Config {
},
Object {
"ALIAS": "net:e3",
"COOKIEJAR": false,
"ENDPOINT": "http://example.net",
"HEADERS": Object {
"hello": "world",
@ -94,7 +139,6 @@ Config {
},
Object {
"ALIAS": "net:posts",
"COOKIEJAR": false,
"ENDPOINT": "http://example.net",
"HEADERS": Object {
"hello": "world",
@ -104,7 +148,6 @@ Config {
},
Object {
"ALIAS": "info:posts",
"COOKIEJAR": false,
"ENDPOINT": "http://example.info",
"HEADERS": Object {
"hello": "mars",
@ -112,46 +155,60 @@ Config {
"REQUEST": "GET /posts",
},
],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
"COOKIEJAR",
],
}
`;
exports[`Config should set up defaults for all requests 1`] = `
Config {
"COOKIEJAR": false,
"CACHE": false,
"DEFAULTS": Object {
"HEADERS": Object {
"authentication": "hello",
},
},
"ENDPOINT": "http://example.com",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Plugins {
"autoload": Array [
"std",
],
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
"PLUGINS": Array [],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"CACHE",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
],
"defaultConfigValues": Object {
"CACHE": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"REQUESTS": Array [
"doc": Object {
"GET /posts/1": "get-post",
"GET /user": Object {
"alias": "user",
"headers": Object {
"hello": "world",
},
},
"defaults": Object {
"HEADERS": Object {
"authentication": "hello",
},
},
"endpoint": "http://jsonplaceholder.typicode.com",
"version": 1,
},
"requests": Array [
Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
@ -159,8 +216,7 @@ Config {
},
Object {
"ALIAS": "user",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
@ -168,15 +224,5 @@ Config {
"REQUEST": "GET /user",
},
],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
"COOKIEJAR",
],
}
`;

View File

@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Beau's plugin system Request Modifiers should modify the request and response using modifiers. 1`] = `
Object {
"body": "Hello World",
"request": Object {
"body": undefined,
"endpoint": "http://example.com/user",
"headers": Object {
"preRequestModifier": true,
},
},
"response": Object {
"body": "Hello World",
"headers": Array [],
"status": 200,
},
}
`;
exports[`Beau's plugin system shouldn't do anything when given an empty array. 1`] = `
Plugins {
"autoload": Array [],
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
}
`;

View File

@ -7,7 +7,7 @@ Object {
"body": Object {
"username": "seich",
},
"endpoint": "http://example.com/user",
"endpoint": "http://martianwabbit.com/user",
"headers": Object {
"authentication": "BEARER abc123",
},
@ -25,7 +25,7 @@ Object {
"body": "{\\"hello\\": \\"world\\"}",
"request": Object {
"body": undefined,
"endpoint": "http://example.com/user",
"endpoint": "http://martianwabbit.com/user",
"headers": undefined,
},
"response": Object {
@ -35,3 +35,22 @@ Object {
},
}
`;
exports[`Request should use modifiers 1`] = `
Array [
Array [
Object {
"endpoint": "http://martianwabbit.com/user",
"headers": Object {},
"method": "GET",
"payload": Object {},
"query": Object {},
},
Object {
"alias": "show",
"endpoint": "http://martianwabbit.com",
"request": "GET /user",
},
],
]
`;

View File

@ -3,6 +3,7 @@
exports[`RequestList should execute requests by alias. 1`] = `
Object {
"body": "{\\"hello\\": \\"world\\"}",
"changed": true,
"request": Object {
"body": Object {
"lastname": "Diaz",
@ -18,26 +19,3 @@ Object {
},
}
`;
exports[`RequestList should fetch dependencies 1`] = `
RequestCache {
"$cache": Object {
"env": Object {
"environmental": true,
},
"get-posts": Object {
"body": "{\\"hello\\": \\"world\\"}",
"request": Object {
"body": undefined,
"endpoint": "http://martianwabbit.com/post",
"headers": undefined,
},
"response": Object {
"body": "{\\"hello\\": \\"world\\"}",
"headers": Array [],
"status": 200,
},
},
},
}
`;

View File

@ -1,59 +1,26 @@
const yaml = require('js-yaml')
const Beau = require('../beau')
const { moduleVersion } = require('../shared')
jest.mock('../shared')
const requireg = require('requireg')
requireg.resolving = false
const yaml = require('js-yaml');
const Beau = require('../beau');
describe(`Beau's config Loader.`, () => {
it('should load the config', () => {
moduleVersion.mockReturnValue(1)
const doc = yaml.load(`
it('should create a request list', () => {
const doc = yaml.safeLoad(`
version: 1
endpoint: 'http://example.com'
endpoint: 'http://jsonplaceholder.typicode.com'
defaults:
headers:
authentication: hello
`)
const beau = new Beau(doc)
expect(beau).toMatchSnapshot()
})
it(`should load the request list using the configuration`, () => {
moduleVersion.mockReturnValue(1)
const doc = yaml.load(`
version: 1
endpoint: 'http://example.com'
GET /posts/1: get-post
GET /user:
alias: user
headers:
hello: world
`)
`);
const beau = new Beau(doc)
expect(beau.requests).toMatchSnapshot()
})
const beau = new Beau(doc);
it('should display a warning if the module version and the beau file version are different', () => {
let stdout
let spy = jest
.spyOn(console, 'warn')
.mockImplementation((val) => (stdout = val))
moduleVersion.mockReturnValue(2)
const beau = new Beau({ version: 1 })
expect(stdout).toEqual('This Beau file expects v1. You are using v2.')
spy.mockReset()
spy.mockRestore()
})
})
expect(beau.requests).toBeDefined();
expect(beau).toMatchSnapshot();
});
});

View File

@ -1,25 +1,24 @@
const yaml = require('js-yaml')
const Config = require('../config')
const requireg = require('requireg')
requireg.resolving = false
const yaml = require('js-yaml');
const Config = require('../config');
describe('Config', () => {
it('should load valid config keys', () => {
const doc = yaml.load(`
it('should load valid config keys', () => {
const doc = yaml.safeLoad(`
version: 1
endpoint: http://martianwabbit.com
cache: false
shouldntBeAdded: true
`)
`);
const config = new Config(doc)
expect(config.ENDPOINT).toBe(doc.endpoint)
expect(config.VERSION).toBe(doc.version)
expect(config.shouldntBeAdded).toBeUndefined()
})
const config = new Config(doc);
expect(config.ENDPOINT).toBe(doc.endpoint);
expect(config.CACHE).toBe(doc.cache);
expect(config.VERSION).toBe(doc.version);
expect(config.shouldntBeAdded).toBeUndefined();
});
it('should load requests', () => {
const doc = yaml.load(`
it('should load requests', () => {
const doc = yaml.safeLoad(`
endpoint: http://example.com
GET /profile: get-profile
@ -29,16 +28,16 @@ describe('Config', () => {
alias: user
headers:
hello: world
`)
`);
const config = new Config(doc)
expect(Object.keys(config.REQUESTS).length).toBe(4)
})
const config = new Config(doc);
expect(Object.keys(config.requests).length).toBe(4);
});
it('should set up defaults for all requests', () => {
const doc = yaml.load(`
it('should set up defaults for all requests', () => {
const doc = yaml.safeLoad(`
version: 1
endpoint: 'http://example.com'
endpoint: 'http://jsonplaceholder.typicode.com'
defaults:
HEADERS:
@ -49,19 +48,18 @@ describe('Config', () => {
alias: user
headers:
hello: world
`)
`);
const config = new Config(doc)
const config = new Config(doc);
expect(config).toMatchSnapshot()
Object.values(config.REQUESTS).forEach((r) => {
expect(r.HEADERS.authentication).toMatch('hello')
})
})
expect(config).toMatchSnapshot();
Object.values(config.requests).forEach(r => {
expect(r.HEADERS.authentication).toMatch('hello');
});
});
it('should load multiple hosts', () => {
const doc = yaml.load(`
version: 1
it('should load multiple hosts', () => {
const doc = yaml.safeLoad(`
endpoint: http://example.org
defaults:
@ -97,15 +95,15 @@ describe('Config', () => {
endpoint: http://example.info
GET /posts: posts
`)
`);
let config = new Config(doc)
let config = new Config(doc);
expect(config).toMatchSnapshot()
})
expect(config).toMatchSnapshot();
});
it('should namespace all aliases within an host', () => {
const doc = yaml.load(`
it('should namespace all aliases within an host', () => {
const doc = yaml.safeLoad(`
hosts:
- host: test1
endpoint: http://example.com
@ -113,16 +111,16 @@ describe('Config', () => {
- host: test2
endpoint: http://example.net
GET /posts: posts
`)
`);
let config = new Config(doc)
let config = new Config(doc);
expect(config.REQUESTS[0].ALIAS).toBe('test1:posts')
expect(config.REQUESTS[1].ALIAS).toBe('test2:posts')
})
expect(config.requests[0].ALIAS).toBe('test1:posts');
expect(config.requests[1].ALIAS).toBe('test2:posts');
});
it(`should throw if host doesn't have a host key`, () => {
const doc = yaml.load(`
it(`should throw if host doesn't have a host key`, () => {
const doc = yaml.safeLoad(`
hosts:
- endpoint: http://example.com
GET /posts: posts
@ -130,13 +128,13 @@ describe('Config', () => {
- host: test2
endpoint: http://example.net
GET /posts: posts
`)
`);
expect(() => new Config(doc)).toThrow()
})
expect(() => new Config(doc)).toThrow();
});
it(`should merge host settings with global settings`, () => {
const doc = yaml.load(`
it(`should merge host settings with global settings`, () => {
const doc = yaml.safeLoad(`
defaults:
headers:
hello: 1
@ -152,25 +150,9 @@ describe('Config', () => {
headers: false
GET /posts: posts
`)
`);
let config = new Config(doc)
expect(config.REQUESTS[0].HEADERS.hello).toBe(1)
})
it(`should allow different settings for the same request`, () => {
const doc = yaml.load(`
host: https://example.com
GET /1:
- alias: req1
headers:
request: 1
- alias: req2
headers:
request: 2
`)
let config = new Config(doc)
expect(config.REQUESTS.length).toBe(2)
})
})
let config = new Config(doc);
expect(config.requests[0].HEADERS.hello).toBe(1);
});
});

View File

@ -1,101 +0,0 @@
const Plugins = require('../plugins')
const Request = require('../request')
const RequestCache = require('../requestCache')
const requireg = require('requireg')
describe(`Beau's plugin system`, () => {
let request
let plugins
beforeEach(() => {
plugins = new Plugins([{ Modifiers: [Object] }, 'DynamicValues'], [])
})
it('should load all plugins', () => {
expect(plugins.registry.preRequestModifiers.length).toBe(1)
expect(plugins.registry.postRequestModifiers.length).toBe(1)
expect(plugins.registry.dynamicValues.length).toBe(1)
})
it(`should throw if given an invalid configuration`, () => {
expect(() => new Plugins([{ test1: true, test2: true }])).toThrow()
})
it(`shouldn't do anything when given an empty array.`, () => {
expect(new Plugins([], [])).toMatchSnapshot()
})
it(`should warn if the plugin is not available.`, () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
requireg.resolving = false
new Plugins(['not-a-Package'])
expect(spy).toHaveBeenCalled()
requireg.resolving = true
spy.mockReset()
spy.mockRestore()
})
describe(`Request Modifiers`, () => {
beforeEach(() => {
request = new Request(
{
request: 'POST /user',
endpoint: 'http://example.com',
alias: 'update'
},
plugins
)
})
it(`should modify the request and response using modifiers.`, async () => {
await expect(request.exec()).resolves.toMatchSnapshot()
})
})
describe(`Dynamic Values`, () => {
beforeEach(() => {
request = new Request(
{
request: 'PATCH /hello/$[add(1, 2)]',
endpoint: 'http://example.com',
alias: 'say-hello',
headers: {
count: '$[add(1, $value2)]',
empty: ''
},
payload: 'counted $[add(1, $value2)] so far.'
},
plugins
)
})
let cache = new RequestCache()
cache.add('value2', '2')
it(`should look for dynamic values executing and replacing them`, async () => {
let req = await request.exec(cache)
expect(req).toHaveProperty('request.body', 'counted 3 so far.')
})
it(`should change the internal datatype if the only thing in the value is the dynamic value`, async () => {
let req = await request.exec(cache)
expect(req).toHaveProperty('request.headers.count', 3)
})
it(`should return empty values as empty`, async () => {
let req = await request.exec(cache)
expect(req).toHaveProperty('request.headers.empty', '')
})
it(`should throw when calling an undefined dynamic value`, async () => {
request = new Request({
request: 'POST /hello/$[notAvailable(1, 2)]',
alias: 'say-hello'
})
await expect(request.exec()).rejects.toThrow()
})
})
})

View File

@ -1,100 +1,94 @@
const Request = require('../request')
const RequestCache = require('../requestCache')
const requestPromiseNativeMock = require('request-promise-native')
const Request = require('../request');
const RequestCache = require('../requestCache');
const RequestList = require('../requestList');
const requestPromiseNativeMock = require('request-promise-native');
describe('Request', () => {
let cache
let validRequestConfig
let invalidRequestConfig
let request
let requestWithoutDependencies
let cache;
let validRequestConfig;
let invalidRequestConfig;
let request;
let requestWithoutDependencies;
beforeEach(() => {
validRequestConfig = {
request: 'POST /user',
endpoint: 'http://example.com',
alias: 'update',
params: {
userId: '$profile.UserId'
},
headers: {
authentication: 'BEARER $session.token'
},
payload: {
username: 'seich'
}
}
beforeEach(() => {
validRequestConfig = {
request: 'POST /user',
endpoint: 'http://martianwabbit.com',
alias: 'update',
params: {
userId: '$profile.UserId'
},
headers: {
authentication: 'BEARER $session.token'
},
payload: {
username: 'seich'
}
};
invalidRequestConfig = {
request: `POST /session`,
endpoint: 'http://example.com'
}
invalidRequestConfig = {
request: `POST /session`,
endpoint: 'http://martianwabbit.com'
};
cache = new RequestCache()
cache.add('session', { token: 'abc123' })
cache.add('profile', { UserId: 14 })
cache = new RequestCache();
cache.add('$session', { token: 'abc123' });
cache.add('$profile', { UserId: 14 });
request = new Request(validRequestConfig)
requestWithoutDependencies = new Request({
endpoint: 'http://example.com',
request: 'GET /user',
alias: 'show'
})
request = new Request(validRequestConfig);
requestWithoutDependencies = new Request({
endpoint: 'http://martianwabbit.com',
request: 'GET /user',
alias: 'show'
});
requestPromiseNativeMock.fail = false
})
requestPromiseNativeMock.fail = false;
});
it('should load up the given request', () => {
expect(request.VERB).toBe('POST')
expect(request.ENDPOINT).toBe(validRequestConfig.endpoint)
expect(request.HEADERS).toBeDefined()
expect(request.PAYLOAD).toBeDefined()
expect(request.PARAMS).toBeDefined()
})
it('should load up the given request', () => {
expect(request.VERB).toBe('POST');
expect(request.ENDPOINT).toBe(validRequestConfig.endpoint + '/user');
expect(request.HEADERS).toBeDefined();
expect(request.PAYLOAD).toBeDefined();
expect(request.PARAMS).toBeDefined();
});
it('should throw if a given request is invalid', () => {
expect(() => new Request(invalidRequestConfig)).toThrow()
})
it('should throw if a given request is invalid', () => {
expect(() => new Request(invalidRequestConfig)).toThrow();
});
it('should list all of its dependencies', () => {
expect(request.DEPENDENCIES.size).toBe(2)
expect(request.DEPENDENCIES).toContain('session')
expect(request.DEPENDENCIES).toContain('profile')
})
it('should list all of its dependencies', () => {
expect(request.DEPENDENCIES.size).toBe(2);
expect(request.DEPENDENCIES).toContain('session');
expect(request.DEPENDENCIES).toContain('profile');
});
it('should execute a request', async () => {
await expect(request.exec(cache)).resolves.toMatchSnapshot()
await expect(
requestWithoutDependencies.exec()
).resolves.toMatchSnapshot()
})
it('should execute a request', async () => {
await expect(request.exec([], cache)).resolves.toMatchSnapshot();
await expect(
requestWithoutDependencies.exec()
).resolves.toMatchSnapshot();
});
it('should throw if the request fails', async () => {
requestPromiseNativeMock.fail = true
await expect(requestWithoutDependencies.exec()).rejects.toThrow(Error)
})
it('should throw if the request fails', async () => {
requestPromiseNativeMock.fail = true;
await expect(requestWithoutDependencies.exec()).rejects.toThrow(Error);
});
it(`should use the full url if given one as part of the path instead of the global endpoint`, async () => {
const requestWithPath = new Request({
endpoint: 'http://example.com',
request: 'GET http://martianwabbit.com/user',
alias: 'get-user'
})
it('should use modifiers', async () => {
const preRequest = jest.fn();
const withPreRequest = [{ preRequest }];
const requestWithoutPath = new Request({
endpoint: 'http://example.com',
request: 'GET /user',
alias: 'get-user'
})
const notCalled = jest.fn();
const nonModifiers = [{ notCalled }];
await expect(requestWithPath.exec()).resolves.toHaveProperty(
'request.endpoint',
'http://martianwabbit.com/user'
)
await requestWithoutDependencies.exec(withPreRequest);
await expect(requestWithoutPath.exec()).resolves.toHaveProperty(
'request.endpoint',
'http://example.com/user'
)
})
})
expect(preRequest).toHaveBeenCalled();
expect(preRequest.mock.calls).toMatchSnapshot();
await requestWithoutDependencies.exec(nonModifiers);
expect(notCalled).not.toHaveBeenCalled();
});
});

View File

@ -1,89 +1,83 @@
const RequestCache = require('../requestCache')
const RequestCache = require('../requestCache');
describe('Request Cache', () => {
let cache
let cache;
beforeEach(() => {
cache = new RequestCache()
beforeEach(() => {
cache = new RequestCache();
cache.add('session', {
hello: 'World'
})
cache.add('$session', {
hello: 'World'
});
cache.add('array', [
{
id: 1,
name: 'Sergio'
},
{
id: 2,
name: 'Angela'
}
])
})
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')
})
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')
})
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.world')).toThrow()
})
})
it('should throw when given an invalid path', () => {
expect(() => cache.get('$session.world')).toThrow();
});
});
describe('parse', () => {
it("should transform variables in strings using it's cache", () => {
expect(cache.parse('Hello $session.hello')).toBe('Hello World')
})
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 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 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')
})
it('should parse arrays as well', () => {
let parsed = cache.parse({ hello: '$array.0.name' });
expect(parsed.hello).toBe('Sergio');
});
it('should return an object when given an undefined value', () => {
expect(cache.parse(undefined)).toEqual({})
})
it('should return an object when given an undefined value', () => {
expect(Object.keys(cache.parse(undefined)).length).toBe(0);
});
it('should parse any value other than undefined', () => {
expect(cache.parse('Hello $session.hello')).toBe('Hello World')
})
it('should parse any value other than undefined', () => {
expect(cache.parse('Hello $session.hello')).toBe('Hello World');
});
it('should return null when passed null', () => {
expect(cache.parse(null)).toBe(null)
})
it(`shouldn't replace escaped variables`, () => {
expect(cache.parse(`\\$session.hello is $session.hello`)).toBe(
`$session.hello is World`
)
})
})
})
it('should return null when passed null', () => {
expect(cache.parse(null)).toBe(null);
});
});
});

View File

@ -1,78 +1,88 @@
const Config = require('../config')
const RequestList = require('../requestList')
const requestPromiseNativeMock = require('request-promise-native')
const Config = require('../config');
const RequestList = require('../requestList');
const requestPromiseNativeMock = require('request-promise-native');
describe('RequestList', () => {
const endpoint = 'http://martianwabbit.com'
const endpoint = 'http://martianwabbit.com';
let env = {
environmental: true
}
let env = {
environmental: true
};
const doc = {
ENDPOINT: endpoint,
ENVIRONMENT: env,
'GET /post': { alias: 'get-posts' },
'POST /user': {
alias: 'user',
payload: {
name: 'Sergio',
lastname: 'Diaz'
}
}
}
const doc = {
ENDPOINT: endpoint,
ENVIRONMENT: env,
PLUGINS: [
{
'beau-jwt': {
data: {
secret: 'shhh.',
userId: 412
}
}
},
'beau-document'
],
'GET /post': { alias: 'get-posts' },
'POST /user': {
alias: 'user',
payload: {
name: 'Sergio',
lastname: 'Diaz'
}
}
};
let requests
beforeEach(() => {
requestPromiseNativeMock.fail = false
let requests;
beforeEach(() => {
requestPromiseNativeMock.fail = false;
let config = new Config(doc)
requests = new RequestList(config)
})
let config = new Config(doc);
requests = new RequestList(config.requests, config);
});
it('should allow an empty request list', () => {
requests = new RequestList()
expect(requests.list.length).toBe(0)
})
it('should load valid requests', () => {
expect(requests.list.length).toBe(2);
});
it('should load valid requests', () => {
expect(requests.list.length).toBe(2)
})
it('should fetch dependencies', () => {
requests.fetchDependencies(['get-posts']);
});
it('should fetch dependencies', async () => {
await expect(
requests.fetchDependencies(['get-posts'])
).resolves.toMatchSnapshot()
})
it('should load plugins', () => {
const pluginLessList = new RequestList();
expect(requests.modifiers.length).toBe(2);
expect(pluginLessList.modifiers.length).toBe(0);
});
it('should execute requests by alias.', async () => {
await expect(requests.execByAlias('user')).resolves.toMatchSnapshot()
})
it('should execute requests by alias.', async () => {
await expect(requests.execByAlias('user')).resolves.toMatchSnapshot();
});
it('should fail if the request fails', async () => {
requestPromiseNativeMock.fail = true
await expect(requests.execByAlias('user')).rejects.toThrow()
})
it('should fail if the request fails', async () => {
requestPromiseNativeMock.fail = true;
await expect(requests.execByAlias('user')).rejects.toThrow();
});
it('should return a cached result if available', async () => {
const obj = { test: true }
requests.cache.add('test', obj)
await expect(requests.execByAlias('test')).resolves.toBe(obj)
})
it('should return a cached result if available', async () => {
const obj = { test: true };
requests.cache.add('$test', obj);
await expect(requests.execByAlias('test')).resolves.toBe(obj);
});
it('should fail if the alias is not found', async () => {
await expect(requests.execByAlias('notAnAlias')).rejects.toThrow()
})
it('should fail if the alias is not found', async () => {
await expect(requests.execByAlias('notAnAlias')).rejects.toThrow();
});
it(`should fail if a given request doesn't have an alias`, () => {
let config = new Config({
'GET /hello': {
headers: {
hello: 1
}
}
})
it(`should fail if a given request doesn't have an alias`, () => {
let config = new Config({
'GET /hello': {
headers: {
hello: 1
}
}
});
expect(() => new RequestList(config, config)).toThrow()
})
})
expect(() => new RequestList(config.requests, config)).toThrow();
});
});

View File

@ -1,98 +0,0 @@
const {
requestRegex,
replacementRegex,
dynamicValueRegex,
UpperCaseKeys,
removeOptionalKeys,
toKebabCase,
replaceInObject,
expandPath
} = require('../shared')
describe('Shared Utilities', () => {
describe('requestRegex', () => {
test.each([
['GET /hello', true],
['HEAD /hello', true],
['POST /hello', true],
['PUT /hello', true],
['DELETE /hello', true],
['CONNECT /hello', true],
['OPTIONS /hello', true],
['TRACE /hello', true],
['PATCH /hello', true]
])('should match: %s', (example, expected) => {
expect(requestRegex.test(example)).toBe(expected)
})
})
describe('replacementRegex', () => {
test.each([
['$a.b', ['$a.b']],
['GET /hello/$a.name', ['$a.name']],
['PUT /hi/$a.a/$a.b', ['$a.a', '$a.b']],
[`\\$value`, ['\\$value']]
])('should match: %s', (example, expected) => {
expect(example.match(replacementRegex)).toEqual(expected)
})
})
describe('dynamicValueRegex', () => {
test.each([
['$[test()]', ['$[test()]']],
['$[test(1, 2, 3)]', ['$[test(1, 2, 3)]']],
[`$[test({ \n id: 1 \n })]`, ['$[test({ \n id: 1 \n })]']]
])('should match: %s', (example, expected) => {
expect(example.match(dynamicValueRegex)).toEqual(expected)
})
})
describe('UpperCaseKeys', () => {
it('should uppercase all first-level keys in an object', () => {
let a = { test: 1, Test2: 2 }
expect(UpperCaseKeys(a)).toEqual({ TEST: 1, TEST2: 2 })
})
})
describe('removeOptionalKeys', () => {
it('should remove empty objects from an object', () => {
let a = { b: {}, c: 2, d: {} }
expect(removeOptionalKeys(a, ['b', 'd'])).toEqual({ c: 2 })
})
})
describe('toKebabCase', () => {
it('should convert camel case to kebab case', () => {
expect(toKebabCase('helloWorld')).toBe('hello-world')
})
})
describe('replaceInObject', () => {
it('should replace every value in an object with the output of a function', () => {
let a = { b: 'b', c: 'c' }
expect(replaceInObject(a, (obj) => 'a')).toEqual({ b: 'a', c: 'a' })
})
})
describe('expandPath', () => {
test.each([
['https://alchem.ee', 'api/v1/hello'],
['https://alchem.ee/', '/api/v1/hello'],
['https://alchem.ee', '/api/v1/hello'],
['https://alchem.ee/', 'api/v1/hello']
])(
'should add a base url to the path is the path is not a url: %s, %s',
(url, path) => {
expect(expandPath(url, path)).toEqual(
'https://alchem.ee/api/v1/hello'
)
}
)
it('should return the path if its a fully fledged url on its own', () => {
expect(
expandPath('https://alchem.ee', 'https://martianwabbit.com')
).toEqual('https://martianwabbit.com')
})
})
})

View File

@ -1,20 +1,12 @@
const RequestList = require('./requestList')
const Config = require('./config')
const { moduleVersion } = require('./shared')
const RequestList = require('./requestList');
const Config = require('./config');
class Beau {
constructor(doc, env = {}) {
this.config = new Config(doc, env)
this.requests = new RequestList(this.config)
constructor(doc, env = {}) {
this.config = new Config(doc, env);
if (this.config.VERSION !== moduleVersion()) {
console.warn(
`This Beau file expects v${
this.config.VERSION
}. You are using v${moduleVersion()}.`
)
}
}
this.requests = new RequestList(this.config.requests, this.config);
}
}
module.exports = Beau
module.exports = Beau;

View File

@ -1,100 +1,94 @@
const deepMerge = require('deepmerge')
const { requestRegex, UpperCaseKeys, moduleVersion } = require('./shared')
const Plugins = require('./plugins')
const deepMerge = require('deepmerge');
const { requestRegex, UpperCaseKeys } = require('./shared');
class Config {
constructor(doc, env = {}) {
const defaultConfigValues = {
VERSION: moduleVersion(),
ENDPOINT: '',
PLUGINS: [],
DEFAULTS: {},
ENVIRONMENT: {},
HOSTS: [],
COOKIEJAR: false
}
constructor(doc, env = {}) {
this.defaultConfigValues = {
VERSION: 1,
CACHE: false,
ENDPOINT: '',
PLUGINS: [],
DEFAULTS: {},
ENVIRONMENT: {},
HOSTS: []
};
this.configKeys = Object.keys(defaultConfigValues)
this.configKeys = Object.keys(this.defaultConfigValues);
this.doc = doc;
let config = this.loadConfig(doc)
Object.assign(this, defaultConfigValues, config)
let config = this.loadConfig(doc);
this.configKeys.forEach(k => {
this[k] = config[k] || this.defaultConfigValues[k];
});
this.ENVIRONMENT = deepMerge(this.ENVIRONMENT, env)
this.ENVIRONMENT = deepMerge(this.ENVIRONMENT, env);
this.REQUESTS = []
this.requests = [];
this.loadRequests(doc, {
DEFAULTS: this.DEFAULTS,
ENDPOINT: this.ENDPOINT
})
this.loadRequests(doc, {
DEFAULTS: this.DEFAULTS,
ENDPOINT: this.ENDPOINT
});
this.loadHosts(this.HOSTS, config, defaultConfigValues)
this.loadHosts(this.HOSTS, config);
}
this.PLUGINS = new Plugins(this.PLUGINS)
}
loadHosts(hosts, rootConfig) {
hosts.forEach(host => {
if (typeof host.host === 'undefined') {
throw new Error(`Host doesn't indicate it's host name.`);
}
loadHosts(hosts, rootConfig, defaultConfigValues) {
hosts.forEach((host) => {
if (typeof host.host === 'undefined') {
throw new Error(`Host doesn't indicate it's host name.`)
}
let config = deepMerge(
this.defaultConfigValues,
this.loadConfig(host)
);
let config = deepMerge(defaultConfigValues, this.loadConfig(host))
config.DEFAULTS = deepMerge(rootConfig.DEFAULTS, config.DEFAULTS);
config.DEFAULTS = deepMerge(rootConfig.DEFAULTS, config.DEFAULTS)
this.loadRequests(host, {
DEFAULTS: config.DEFAULTS,
ENDPOINT: config.ENDPOINT,
NAMESPACE: host.host
});
});
}
this.loadRequests(host, {
DEFAULTS: config.DEFAULTS,
ENDPOINT: config.ENDPOINT,
NAMESPACE: host.host
})
})
}
loadRequests(host, settings) {
let requests = Object.keys(host)
.filter(key => requestRegex.test(key))
.map(key => {
let requestDefinitionIsString = typeof host[key] === 'string';
let originalRequest = requestDefinitionIsString
? { ALIAS: host[key] }
: deepMerge.all([host[key]]);
loadRequests(host, settings) {
Object.entries(host)
.filter(([key]) => requestRegex.test(key))
.forEach(([key, rDefinition]) => {
if (Array.isArray(rDefinition)) {
rDefinition.forEach((req) =>
this.addRequest(key, req, settings)
)
} else {
this.addRequest(key, rDefinition, settings)
}
})
}
let request = UpperCaseKeys(originalRequest);
addRequest(key, rDefinition, settings) {
let requestDefinitionIsString = typeof rDefinition === 'string'
let originalRequest = requestDefinitionIsString
? { ALIAS: rDefinition }
: rDefinition
if (settings.NAMESPACE) {
request.ALIAS = `${settings.NAMESPACE}:${request.ALIAS}`;
}
let request = UpperCaseKeys(originalRequest)
request.REQUEST = key;
request.ENDPOINT = settings.ENDPOINT;
if (settings.NAMESPACE) {
request.ALIAS = `${settings.NAMESPACE}:${request.ALIAS}`
}
let defaults = UpperCaseKeys(settings.DEFAULTS);
request.REQUEST = key
request.COOKIEJAR = this.COOKIEJAR
request.ENDPOINT = settings.ENDPOINT
return deepMerge(defaults, request);
});
let defaults = UpperCaseKeys(settings.DEFAULTS)
this.requests = this.requests.concat(requests);
}
this.REQUESTS.push(deepMerge(defaults, request))
}
loadConfig(host) {
let config = {};
loadConfig(host) {
let config = {}
Object.keys(host)
.filter(k => this.configKeys.includes(k.toUpperCase()))
.forEach(k => (config[k.toUpperCase()] = host[k]));
Object.entries(host)
.filter(([key]) => this.configKeys.includes(key.toUpperCase()))
.forEach(([key, value]) => (config[key.toUpperCase()] = value))
return config
}
return config;
}
}
module.exports = Config
module.exports = Config;

View File

@ -1,119 +0,0 @@
const vm = require('vm')
const requireg = require('requireg')
const deepmerge = require('deepmerge')
const { toKebabCase, dynamicValueRegex, replaceInObject } = require('./shared')
const { isPlainObject } = require('is-plain-object')
class Plugins {
constructor(plugins = [], autoload = ['std']) {
this.registry = {
preRequestModifiers: [],
postRequestModifiers: [],
dynamicValues: []
}
this.context = {}
this.autoload = autoload
this.loadPlugins(plugins.concat(this.autoload))
}
normalizePlugins(plugins) {
let results = {}
plugins.forEach((plugin) => {
let name = plugin
let settings = undefined
if (typeof plugin === 'object') {
let keys = Object.keys(plugin)
if (keys.length !== 1) {
throw new Error(`Plugin items should contain only one key.`)
}
name = keys[0]
settings = plugin[name]
}
results[name] = settings
})
return results
}
loadPlugins(plugins) {
plugins = this.normalizePlugins(plugins)
Object.keys(plugins).forEach((name) => {
const module = `beau-${toKebabCase(name)}`
if (typeof requireg.resolve(module) !== 'undefined') {
const plugin = requireg(module)
new plugin(this, plugins[name])
} else {
if (this.autoload.includes(name)) return
console.warn(
`Plugin ${name} couldn't be found. It is available globally?`
)
}
})
}
executeModifier(modifier, obj, orig) {
let result = deepmerge({}, obj, { isMergeableObject: isPlainObject })
this.registry[modifier].forEach(
(modifier) => (result = modifier(result, orig))
)
return result
}
replaceDynamicValues(obj) {
vm.createContext(this.context)
return replaceInObject(obj, (val) => {
let valIsEmpty = val.trim().length === 0
if (valIsEmpty) {
return val
}
try {
let onlyHasDynamic =
val.replace(dynamicValueRegex, '').trim() === ''
if (onlyHasDynamic) {
let call
val.replace(dynamicValueRegex, (match, c) => {
call = c
})
return vm.runInContext(call, this.context)
}
return val.replace(dynamicValueRegex, (match, call) => {
return vm.runInContext(call, this.context)
})
} catch (e) {
throw new Error(`DynamicValue: ` + e)
}
})
}
addPreRequestModifier(modifier) {
this.registry.preRequestModifiers.push(modifier)
}
addPostRequestModifier(modifier) {
this.registry.postRequestModifiers.push(modifier)
}
defineDynamicValue(name, fn) {
this.registry.dynamicValues.push({ name, fn })
this.context[name] = fn
}
}
module.exports = Plugins

View File

@ -1,131 +1,126 @@
const request = require('request-promise-native')
const RequestCache = require('./requestCache')
const Plugins = require('./plugins')
const request = require('request-promise-native');
const {
requestRegex,
replacementRegex,
UpperCaseKeys,
removeOptionalKeys,
isUrl
} = require('./shared')
httpVerbs,
requestRegex,
replacementRegex,
UpperCaseKeys,
removeOptionalKeys
} = require('./shared');
const RequestList = require('./requestList');
const RequestCache = require('./requestCache');
class Request {
constructor(req, plugins = new Plugins()) {
this.originalRequest = req
this.plugins = plugins
constructor(req) {
this.originalRequest = req;
req = UpperCaseKeys(req)
Object.assign(this, req)
const {
REQUEST,
ALIAS,
PAYLOAD,
ENDPOINT,
PARAMS,
HEADERS
} = UpperCaseKeys(req);
if (!this.ALIAS) {
throw new Error(`${this.REQUEST} is missing an alias.`)
}
if (!ALIAS) {
throw new Error(`${REQUEST} is missing an alias.`);
}
const { VERB, PATH } = this.parseRequest(this.REQUEST)
const { verb, path } = this.parseRequest(REQUEST);
this.VERB = VERB
this.PATH = PATH
this.VERB = verb;
this.ENDPOINT = ENDPOINT + path;
this.DEPENDENCIES = this.findDependencies(req)
}
this.HEADERS = HEADERS;
this.PAYLOAD = PAYLOAD;
this.PARAMS = PARAMS;
parseRequest(request) {
const parts = request.match(requestRegex)
this.ALIAS = ALIAS;
return {
VERB: parts[1],
PATH: parts[2]
}
}
this.DEPENDENCIES = this.findDependencies(req);
}
findDependencies(request, set = new Set()) {
let type = typeof request
parseRequest(request) {
const parts = request.match(requestRegex);
if (type === 'object' && request !== null) {
Object.entries(request)
.filter(([key]) => key !== 'ALIAS')
.forEach(([key, value]) => {
set = this.findDependencies(value, set)
})
} else if (type === 'string') {
const matches = []
request.replace(
replacementRegex,
(match, g1) => !match.startsWith('\\') && matches.push(g1)
)
return {
verb: parts[1],
path: parts[2]
};
}
const deps = matches.map((m) => m.split('.')[0])
findDependencies(request, set = new Set()) {
if (typeof request === 'object') {
const keys = Object.keys(request).filter(key => key !== 'ALIAS');
return new Set([...set, ...deps])
}
keys.forEach(key => {
set = this.findDependencies(request[key], set);
});
} else if (typeof request === 'string') {
const matches = request.match(replacementRegex) || [];
const deps = matches.map(m => m.split('.')[0].substring(1));
return set
}
return new Set([...set, ...deps]);
}
async exec(cache = new RequestCache()) {
let settings = cache.parse({
baseUrl: '',
uri: this.PATH,
method: this.VERB,
jar: this.COOKIEJAR,
return set;
}
headers: this.HEADERS,
qs: this.PARAMS,
body: this.PAYLOAD,
form: this.FORM,
formData: this.FORMDATA,
async exec(modifiers = [], cache = new RequestCache()) {
const settings = {
endpoint: cache.parse(this.ENDPOINT),
method: this.VERB,
headers: cache.parse(this.HEADERS),
query: cache.parse(this.PARAMS),
payload: cache.parse(this.PAYLOAD)
};
json: true,
simple: false,
resolveWithFullResponse: true
})
modifiers.forEach(mod => {
if (typeof mod.preRequest !== 'undefined') {
mod.preRequest(settings, this.originalRequest);
}
});
const isPathFullUrl = isUrl(settings.uri)
settings.baseUrl = isPathFullUrl ? '' : this.ENDPOINT
try {
const response = await request(
removeOptionalKeys(
{
url: settings.endpoint,
method: settings.method,
settings = removeOptionalKeys(settings, [
'headers',
'qs',
'body',
'form',
'formData'
])
headers: settings.headers,
qs: settings.query,
body: settings.payload,
settings = this.plugins.replaceDynamicValues(settings)
json: true,
simple: false,
resolveWithFullResponse: true
},
['headers', 'qs', 'body']
)
);
settings = this.plugins.executeModifier(
'preRequestModifiers',
settings,
this.originalRequest
)
const results = {
request: {
headers: response.request.headers,
body: response.request.body,
endpoint: response.request.uri.href
},
response: {
status: response.statusCode,
headers: response.headers,
body: response.body
},
body: response.body
};
const response = await request(settings)
cache.add(`$${this.ALIAS}`, results);
let results = {
request: {
headers: response.request.headers,
body: response.request.body,
endpoint: response.request.uri.href
},
response: {
status: response.statusCode,
headers: response.headers,
body: response.body
},
body: response.body
}
results = this.plugins.executeModifier(
'postRequestModifiers',
results,
this.originalRequest
)
cache.add(this.ALIAS, results)
return results
}
return results;
} catch ({ error }) {
throw new Error(error);
}
}
}
module.exports = Request
module.exports = Request;

View File

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

View File

@ -1,54 +1,93 @@
const Request = require('./request')
const RequestCache = require('./requestCache')
const Request = require('./request');
const RequestCache = require('./requestCache');
const httpVerbs = require('./shared').httpVerbs;
const requireg = require('requireg');
class RequestList {
constructor(config = { REQUESTS: [] }) {
this.list = this.loadRequests(config.REQUESTS, config.PLUGINS)
this.cache = new RequestCache()
constructor(requests = [], config = {}) {
this.config = config;
this.requests = requests;
this.cache.add(`env`, config.ENVIRONMENT)
}
this.modifiers = this.loadPlugins();
this.list = this.loadRequests();
this.cache = new RequestCache();
async execByAlias(alias) {
if (this.cache.exists(alias)) {
return this.cache.get(alias)
}
this.cache.add(`$env`, this.config.ENVIRONMENT);
}
const request = this.list.find((r) => r.ALIAS === alias)
async execByAlias(alias) {
if (this.cache.exists(`$${alias}`)) {
return this.applyPostResponseModifiers(this.cache.get(`$${alias}`));
}
if (typeof request === 'undefined') {
throw new Error(`${alias} not found among the requests.`)
}
const request = this.list.find(r => r.ALIAS === alias);
try {
await this.fetchDependencies(Array.from(request.DEPENDENCIES))
return await request.exec(this.cache)
} catch (reason) {
throw new Error(
`Request ${request.VERB} ${request.ENDPOINT} FAILED. \n${reason}`
)
}
}
if (typeof request === 'undefined') {
throw new Error(`${alias} not found among the requests.`);
}
async fetchDependencies(dependencies) {
dependencies = dependencies.map((d) => this.execByAlias(d))
await Promise.all(dependencies)
try {
await this.fetchDependencies(Array.from(request.DEPENDENCIES));
const response = await request.exec(this.modifiers, this.cache);
return this.cache
}
return this.applyPostResponseModifiers(response);
} catch (reason) {
throw new Error(
`Request: ${request.VERB} ${
request.ENDPOINT
} FAILED. \n${reason}`
);
}
}
loadRequests(REQUESTS, PLUGINS) {
let requests = []
REQUESTS.forEach((request) => {
try {
requests.push(new Request(request, PLUGINS))
} catch (e) {
throw new Error(`${request.request} was ignored: ${e}`)
}
})
async fetchDependencies(dependencies) {
dependencies = dependencies.map(d => this.execByAlias(d));
await Promise.all(dependencies);
return requests
}
return this.cache;
}
loadRequests() {
let requests = [];
this.requests.forEach(request => {
try {
let r = new Request(request);
requests.push(r);
} catch (e) {
throw new Error(`${request.request} was ignored: ${e}`);
}
});
return requests;
}
loadPlugins() {
if (typeof this.config.PLUGINS === 'undefined') {
return [];
}
return this.config.PLUGINS.map(plugin => {
let name = plugin;
let settings = null;
if (typeof plugin === 'object') {
name = Object.keys(plugin)[0];
settings = plugin[name];
}
return new (requireg(name))(settings);
});
}
applyPostResponseModifiers(response) {
this.modifiers.forEach(mod => {
if (typeof mod.postResponse !== 'undefined') {
mod.postResponse(response);
}
});
return response;
}
}
module.exports = RequestList
module.exports = RequestList;

View File

@ -1,99 +1,46 @@
const { URL } = require('url')
const httpVerbs = [
'GET',
'HEAD',
'POST',
'PUT',
'DELETE',
'CONNECT',
'OPTIONS',
'TRACE',
'PATCH'
]
'GET',
'HEAD',
'POST',
'PUT',
'DELETE',
'CONNECT',
'OPTIONS',
'TRACE',
'PATCH'
];
const requestRegex = new RegExp(`(${httpVerbs.join('|')})\\s(.*)`, 'i')
const replacementRegex = /(?:\\?)\$([a-zA-Z\.\d\-\_\:]+)/g
const dynamicValueRegex = /\$\[(\w+\((?:.|[\n\r])*?\))\]/g
const requestRegex = new RegExp(`(${httpVerbs.join('|')})\\s(.*)`, 'i');
const replacementRegex = /\$([a-zA-Z\.\d\-\_\/\\\:]*)/g;
const UpperCaseKeys = function (obj) {
let result = {}
Object.entries(obj).forEach(([k, v]) => (result[k.toUpperCase()] = v))
return result
}
const UpperCaseKeys = function(obj) {
let result = {};
Object.keys(obj).forEach(k => (result[k.toUpperCase()] = obj[k]));
return result;
};
const isEmptyObject = (obj) =>
Object.keys(obj).length === 0 && obj.constructor === Object
const removeOptionalKeys = function(obj, optionalValues) {
let result = {};
const removeOptionalKeys = function (obj, optionalValues) {
let result = {}
Object.keys(obj).forEach(key => {
if (
optionalValues.includes(key) &&
(Object.keys(obj[key]).length === 0 &&
obj[key].constructor === Object)
) {
return;
}
Object.entries(obj).forEach(([key, value]) => {
if (optionalValues.includes(key) && isEmptyObject(value)) {
return
}
result[key] = obj[key];
});
result[key] = value
})
return result
}
const toKebabCase = function (str) {
return str
.trim()
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase()
}
const replaceInObject = function (obj, fn) {
if (obj === null) {
return null
}
switch (typeof obj) {
case 'undefined':
return {}
case 'string':
return fn(obj)
case 'object':
obj = Object.assign({}, obj)
Object.entries(obj).forEach(
([key, value]) => (obj[key] = replaceInObject(value, fn))
)
default:
return obj
}
}
const moduleVersion = () => parseInt(require('../package.json').version, 10)
const isUrl = function (str) {
try {
new URL(str)
return true
} catch (e) {
return false
}
}
const expandPath = (url, path) => {
if (isUrl(path)) {
return path
}
return url.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '')
}
return result;
};
module.exports = {
requestRegex,
replacementRegex,
dynamicValueRegex,
UpperCaseKeys,
removeOptionalKeys,
toKebabCase,
replaceInObject,
moduleVersion,
isUrl,
expandPath
}
httpVerbs,
requestRegex,
replacementRegex,
UpperCaseKeys,
removeOptionalKeys
};

2942
yarn.lock Normal file

File diff suppressed because it is too large Load Diff