Compare commits

...

150 Commits

Author SHA1 Message Date
dependabot-preview[bot] eee0aa95ea
Bump js-yaml from 3.14.0 to 4.0.0 (#186)
* Bump js-yaml from 3.14.0 to 4.0.0

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.0 to 4.0.0.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.0...4.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* Updating js-yaml to 4.0

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: David Diaz <seich@martianwabbit.com>
2021-01-29 13:16:29 -06:00
David Díaz b54d2158df
Fixed how endpoints are displayed on the list command. (#190) 2021-01-29 12:49:05 -06:00
dependabot-preview[bot] 8b72a42af1
Bump better-ajv-errors from 0.6.7 to 0.7.0 (#189)
Bumps [better-ajv-errors](https://github.com/atlassian/better-ajv-errors) from 0.6.7 to 0.7.0.
- [Release notes](https://github.com/atlassian/better-ajv-errors/releases)
- [Changelog](https://github.com/atlassian/better-ajv-errors/blob/master/CHANGELOG.md)
- [Commits](https://github.com/atlassian/better-ajv-errors/compare/better-ajv-errors@0.6.7...better-ajv-errors@0.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-01-12 19:57:07 -06:00
dependabot-preview[bot] ac432305b9
Bump globby from 11.0.1 to 11.0.2 (#188)
Bumps [globby](https://github.com/sindresorhus/globby) from 11.0.1 to 11.0.2.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v11.0.1...v11.0.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-01-12 19:53:35 -06:00
dependabot-preview[bot] 98c6b4d941
Bump ajv from 7.0.0-beta.6 to 7.0.3 (#187)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 7.0.0-beta.6 to 7.0.3.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v7.0.0-beta.6...v7.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-01-12 19:49:41 -06:00
dependabot-preview[bot] 1027825091
Bump np from 7.0.0 to 7.2.0 (#185)
Bumps [np](https://github.com/sindresorhus/np) from 7.0.0 to 7.2.0.
- [Release notes](https://github.com/sindresorhus/np/releases)
- [Commits](https://github.com/sindresorhus/np/compare/v7.0.0...v7.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-01-12 19:46:22 -06:00
dependabot[bot] 7e7ae395f9
Bump node-notifier from 8.0.0 to 8.0.1 (#183)
Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/mikaelbr/node-notifier/releases)
- [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md)
- [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-12 19:26:52 -06:00
dependabot-preview[bot] e0de623d1a
Bump @oclif/plugin-help from 3.2.0 to 3.2.1 (#179)
Bumps [@oclif/plugin-help](https://github.com/oclif/plugin-help) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/oclif/plugin-help/releases)
- [Changelog](https://github.com/oclif/plugin-help/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/plugin-help/compare/v3.2.0...v3.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-01-12 19:23:28 -06:00
dependabot-preview[bot] 92485a5277
[Security] Bump ini from 1.3.5 to 1.3.8 (#176)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. **This update includes a security fix.**
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-01-12 19:06:39 -06:00
dependabot[bot] 3175e8c142
Bump ini from 1.3.5 to 1.3.7 (#175)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-06 15:01:53 -06:00
David Diaz 2302763503 0.11.3 2020-11-26 16:07:27 -06:00
David Diaz fd2cc609ea Fixed typo. 2020-11-26 16:06:22 -06:00
David Diaz cdd0c93dee 0.11.2 2020-11-22 01:04:47 -06:00
David Díaz f82a529ebe
Improved prompt descriptions. (#167)
I was really naive with combining urls and paths. This should fix it.
2020-11-22 01:03:59 -06:00
David Díaz 3cb6851ead
Updated license. (#166) 2020-11-21 23:32:49 -06:00
David Diaz 78596a154d 0.11.1 2020-11-21 22:59:17 -06:00
David Díaz dafd4406c1
Schema should be validated on PR (#165) 2020-11-21 22:57:44 -06:00
David Díaz 7adba8e3dc
Added some missing descriptions to the document schema. (#164) 2020-11-21 22:29:49 -06:00
David Díaz 849b823311
Update example (#163)
* Moving from inquirer to prompts.

It has a smaller dependency graph and we don't need the fancy features
inquirer has.

* Added a single better example to keep updated as I finalize the schema.
2020-11-21 21:59:03 -06:00
David Díaz 987c1df81e
Moving from inquirer to prompts. (#162)
It has a smaller dependency graph and we don't need the fancy features
inquirer has.
2020-11-21 21:55:57 -06:00
David Diaz d837be77de 0.11.0 2020-11-21 20:02:46 -06:00
David Díaz 2af8e5492f
Cli tests should be ran too. (#161)
* Cli tests should be ran too.

* Updating snapshot.
2020-11-21 20:02:01 -06:00
David Díaz d6c6c0c7e4
Added an interactive flag to request. (#160)
If present you get to choose what request you want to make from a
dropdown list.
2020-11-20 18:47:55 -06:00
David Díaz 44919334be
Formatted all code with new prettier conf. (#159) 2020-11-20 17:52:13 -06:00
David Diaz a9eb8c97dd Adding tests badge. 2020-11-20 17:44:14 -06:00
David Diaz fa28dbee8b Updating badges. 2020-11-20 17:41:58 -06:00
David Díaz 9decb4978b
Updating dependencies that require manual input. (#158)
* Updated is-plain-object.

* Updated jest.

* Moving from circleci to github actions.

* Switching jsome to color-json to reduce dependencies (somehow).

* Updating snaps.
2020-11-20 17:39:56 -06:00
David Díaz 56894bd459
Update deps (#157)
* Updated is-plain-object.

* Updated jest.

* Moving from circleci to github actions.
2020-11-20 17:18:24 -06:00
David Diaz 1e4e0ba885 Changing the schema's title. 2020-11-20 15:39:05 -06:00
David Díaz c73f43aedb
Merge pull request #156 from Seich/json-schema
Moving the schema over to jsonschema.
2020-11-19 23:12:51 -06:00
David Diaz b7d2e56692 Merge branch 'master' into json-schema 2020-11-19 23:09:45 -06:00
David Diaz 3b5e848530 Added better ajv errors.
It prints json for now while I figure a nicer way of displaying errors
in yaml.
2020-11-19 22:06:22 -06:00
David Diaz a39f3b5977 Config files are now validated against the json schema. 2020-11-19 21:44:17 -06:00
David Diaz 5029a09b41 Added a base class to cli commands. 2020-11-19 21:17:36 -06:00
David Diaz df41d4fa0b Removed the validate command.
We'll be replacing it with jsonschema soon.
2020-11-18 18:37:00 -06:00
David Díaz acb39bd21f
Merge pull request #153 from Seich/dependabot/npm_and_yarn/np-7.0.0
Bump np from 6.3.2 to 7.0.0
2020-11-18 18:01:00 -06:00
David Diaz 614b1bf966 Moving the schema over to jsonschema.
It's easier to maintain and should allow us to provide autocompletion in
the future.
2020-11-18 17:53:24 -06:00
dependabot-preview[bot] 162e6f265c
Bump np from 6.3.2 to 7.0.0
Bumps [np](https://github.com/sindresorhus/np) from 6.3.2 to 7.0.0.
- [Release notes](https://github.com/sindresorhus/np/releases)
- [Commits](https://github.com/sindresorhus/np/compare/v6.3.2...v7.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-18 23:43:32 +00:00
David Díaz b6ef0f06a3
Merge pull request #145 from Seich/dependabot/npm_and_yarn/jest-watch-typeahead-0.6.1
Bump jest-watch-typeahead from 0.6.0 to 0.6.1
2020-11-18 17:41:30 -06:00
dependabot-preview[bot] 938c82125c
Bump jest-watch-typeahead from 0.6.0 to 0.6.1
Bumps [jest-watch-typeahead](https://github.com/jest-community/jest-watch-typeahead) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/jest-community/jest-watch-typeahead/releases)
- [Changelog](https://github.com/jest-community/jest-watch-typeahead/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jest-community/jest-watch-typeahead/compare/v0.6.0...v0.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-18 23:33:52 +00:00
David Díaz 2ee372657a
Merge pull request #136 from Seich/dependabot/npm_and_yarn/oclif/config-1.17.0
Bump @oclif/config from 1.16.0 to 1.17.0
2020-11-18 17:31:26 -06:00
dependabot-preview[bot] e97038c6db
Bump @oclif/config from 1.16.0 to 1.17.0
Bumps [@oclif/config](https://github.com/oclif/config) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/oclif/config/releases)
- [Changelog](https://github.com/oclif/config/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/config/compare/v1.16.0...v1.17.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-18 19:38:41 +00:00
David Díaz acdd458af1
Merge pull request #135 from Seich/dependabot/npm_and_yarn/oclif/command-1.8.0
Bump @oclif/command from 1.7.0 to 1.8.0
2020-11-18 13:36:44 -06:00
dependabot-preview[bot] 09eebcf505
Bump @oclif/command from 1.7.0 to 1.8.0
Bumps [@oclif/command](https://github.com/oclif/command) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/oclif/command/releases)
- [Changelog](https://github.com/oclif/command/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/command/compare/v1.7.0...v1.8.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-18 19:33:01 +00:00
David Díaz dd556a85e1
Merge pull request #134 from Seich/dependabot/npm_and_yarn/oclif/plugin-help-3.2.0
Bump @oclif/plugin-help from 3.1.0 to 3.2.0
2020-11-18 13:31:03 -06:00
dependabot-preview[bot] f4f73e2ca8
Bump @oclif/plugin-help from 3.1.0 to 3.2.0
Bumps [@oclif/plugin-help](https://github.com/oclif/plugin-help) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/oclif/plugin-help/releases)
- [Changelog](https://github.com/oclif/plugin-help/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/plugin-help/compare/v3.1.0...v3.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-08-04 11:24:56 +00:00
David Díaz 342eef7915
Merge pull request #130 from Seich/dependabot/npm_and_yarn/request-promise-native-1.0.9
Bump request-promise-native from 1.0.8 to 1.0.9
2020-07-23 12:48:24 -06:00
dependabot-preview[bot] 8cf3f1cecc
Bump request-promise-native from 1.0.8 to 1.0.9
Bumps [request-promise-native](https://github.com/request/request-promise-native) from 1.0.8 to 1.0.9.
- [Release notes](https://github.com/request/request-promise-native/releases)
- [Commits](https://github.com/request/request-promise-native/compare/v1.0.8...v1.0.9)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-23 18:46:42 +00:00
David Díaz 9f1a3532f6
Merge pull request #129 from Seich/dependabot/npm_and_yarn/is-plain-object-4.1.0
Bump is-plain-object from 3.0.1 to 4.1.0
2020-07-23 12:44:44 -06:00
dependabot-preview[bot] 7ac883999f
Bump is-plain-object from 3.0.1 to 4.1.0
Bumps [is-plain-object](https://github.com/jonschlinkert/is-plain-object) from 3.0.1 to 4.1.0.
- [Release notes](https://github.com/jonschlinkert/is-plain-object/releases)
- [Commits](https://github.com/jonschlinkert/is-plain-object/compare/v3.0.1...v4.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-21 11:26:13 +00:00
David Díaz 2b69c79e06
Merge pull request #127 from Seich/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-20 20:36:38 -06:00
dependabot[bot] ef41c5360a
Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-21 02:33:54 +00:00
David Díaz 311e56c609
Merge pull request #126 from Seich/dependabot/npm_and_yarn/np-6.3.2
Bump np from 6.2.3 to 6.3.2
2020-07-20 20:32:54 -06:00
dependabot-preview[bot] 95b55edc39
Bump np from 6.2.3 to 6.3.2
Bumps [np](https://github.com/sindresorhus/np) from 6.2.3 to 6.3.2.
- [Release notes](https://github.com/sindresorhus/np/releases)
- [Commits](https://github.com/sindresorhus/np/compare/v6.2.3...v6.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-21 02:28:16 +00:00
David Díaz 7261bfe76a
Merge pull request #123 from Seich/dependabot/npm_and_yarn/oclif/command-1.7.0
Bump @oclif/command from 1.6.1 to 1.7.0
2020-07-20 20:26:22 -06:00
dependabot-preview[bot] 15fc4dae8d
Bump @oclif/command from 1.6.1 to 1.7.0
Bumps [@oclif/command](https://github.com/oclif/command) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/oclif/command/releases)
- [Changelog](https://github.com/oclif/command/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/command/compare/v1.6.1...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 23:42:54 +00:00
David Díaz d54b99226b
Merge pull request #122 from Seich/dependabot/npm_and_yarn/oclif/config-1.16.0
Bump @oclif/config from 1.15.1 to 1.16.0
2020-07-20 17:40:48 -06:00
dependabot-preview[bot] 3eace272e2
Bump @oclif/config from 1.15.1 to 1.16.0
Bumps [@oclif/config](https://github.com/oclif/config) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/oclif/config/releases)
- [Changelog](https://github.com/oclif/config/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/config/compare/v1.15.1...v1.16.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 23:34:54 +00:00
David Díaz 4381e123a3
Merge pull request #117 from Seich/dependabot/npm_and_yarn/oclif/plugin-help-3.1.0
Bump @oclif/plugin-help from 3.0.0 to 3.1.0
2020-07-20 17:32:48 -06:00
dependabot-preview[bot] 3e18f32c55
Bump @oclif/plugin-help from 3.0.0 to 3.1.0
Bumps [@oclif/plugin-help](https://github.com/oclif/plugin-help) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/oclif/plugin-help/releases)
- [Changelog](https://github.com/oclif/plugin-help/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/plugin-help/compare/v3.0.0...v3.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 23:01:55 +00:00
David Díaz b4479a91e7
Merge pull request #121 from Seich/dependabot/npm_and_yarn/is-plain-object-3.0.1
Bump is-plain-object from 3.0.0 to 3.0.1
2020-07-20 16:59:56 -06:00
dependabot-preview[bot] 9a04b99366
Bump is-plain-object from 3.0.0 to 3.0.1
Bumps [is-plain-object](https://github.com/jonschlinkert/is-plain-object) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/jonschlinkert/is-plain-object/releases)
- [Commits](https://github.com/jonschlinkert/is-plain-object/compare/v3.0.0...v3.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-20 22:58:03 +00:00
David Díaz 4938dc3602
Merge pull request #116 from Seich/dependabot/npm_and_yarn/globby-11.0.1
Bump globby from 11.0.0 to 11.0.1
2020-07-20 16:55:58 -06:00
dependabot-preview[bot] d1d08f4d60
Bump globby from 11.0.0 to 11.0.1
Bumps [globby](https://github.com/sindresorhus/globby) from 11.0.0 to 11.0.1.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v11.0.0...v11.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-12 18:33:48 +00:00
David Díaz 551499f822
Merge pull request #115 from Seich/dependabot/npm_and_yarn/js-yaml-3.14.0
Bump js-yaml from 3.13.1 to 3.14.0
2020-06-12 12:31:46 -06:00
dependabot-preview[bot] 2dfbc5e4c1
Bump js-yaml from 3.13.1 to 3.14.0
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.13.1 to 3.14.0.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.13.1...3.14.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-12 18:24:03 +00:00
David Díaz 5328428298
Merge pull request #113 from Seich/dependabot/npm_and_yarn/handlebars-4.7.6
[Security] Bump handlebars from 4.4.2 to 4.7.6
2020-06-12 12:21:49 -06:00
dependabot-preview[bot] 1e376e625f
[Security] Bump handlebars from 4.4.2 to 4.7.6
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.4.2 to 4.7.6. **This update includes a security fix.**
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.4.2...v4.7.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-14 09:21:44 +00:00
David Díaz 1a526c3869
Merge pull request #112 from Seich/dependabot/npm_and_yarn/oclif/command-1.6.1
Bump @oclif/command from 1.5.20 to 1.6.1
2020-05-08 18:56:52 -06:00
dependabot-preview[bot] 895a6efe90
Bump @oclif/command from 1.5.20 to 1.6.1
Bumps [@oclif/command](https://github.com/oclif/command) from 1.5.20 to 1.6.1.
- [Release notes](https://github.com/oclif/command/releases)
- [Changelog](https://github.com/oclif/command/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/command/compare/v1.5.20...v1.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 23:53:25 +00:00
David Díaz 64c3d29664
Merge pull request #110 from Seich/dependabot/npm_and_yarn/np-6.2.3
Bump np from 6.2.1 to 6.2.3
2020-05-08 17:51:29 -06:00
dependabot-preview[bot] 39cd4c802e
Bump np from 6.2.1 to 6.2.3
Bumps [np](https://github.com/sindresorhus/np) from 6.2.1 to 6.2.3.
- [Release notes](https://github.com/sindresorhus/np/releases)
- [Commits](https://github.com/sindresorhus/np/compare/v6.2.1...v6.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 23:19:15 +00:00
David Díaz f883e6611b
Bump globby from 10.0.1 to 11.0.0 (#109)
Bumps [globby](https://github.com/sindresorhus/globby) from 10.0.1 to 11.0.0.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v10.0.1...v11.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 17:17:18 -06:00
David Díaz 2c478231a6
Merge branch 'master' into dependabot/npm_and_yarn/globby-11.0.0 2020-05-08 17:17:12 -06:00
dependabot-preview[bot] bf121d6e57
Bump globby from 10.0.1 to 11.0.0
Bumps [globby](https://github.com/sindresorhus/globby) from 10.0.1 to 11.0.0.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v10.0.1...v11.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-08 23:17:04 +00:00
dependabot-preview[bot] f5c09d8415
Bump deepmerge from 4.1.1 to 4.2.2 (#108)
Bumps [deepmerge](https://github.com/TehShrike/deepmerge) from 4.1.1 to 4.2.2.
- [Release notes](https://github.com/TehShrike/deepmerge/releases)
- [Changelog](https://github.com/TehShrike/deepmerge/blob/master/changelog.md)
- [Commits](https://github.com/TehShrike/deepmerge/compare/v4.1.1...v4.2.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 17:16:56 -06:00
dependabot-preview[bot] 607c6fcfe4
Bump @oclif/config from 1.15.0 to 1.15.1 (#107)
Bumps [@oclif/config](https://github.com/oclif/config) from 1.15.0 to 1.15.1.
- [Release notes](https://github.com/oclif/config/releases)
- [Changelog](https://github.com/oclif/config/blob/master/CHANGELOG.md)
- [Commits](https://github.com/oclif/config/compare/v1.15.0...v1.15.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 17:15:10 -06:00
greenkeeper[bot] 133423c98d
Update jest-watch-typeahead to the latest version 🚀 (#106)
* chore(package): update jest-watch-typeahead to version 0.6.0

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 17:10:17 -06:00
greenkeeper[bot] 57ab2ad9ab
Update @oclif/plugin-help to the latest version 🚀 (#105)
* fix(package): update @oclif/plugin-help to version 3.0.0

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 17:08:49 -06:00
greenkeeper[bot] 4db7f7f5a7
Update @oclif/command to the latest version 🚀 (#104)
* fix(package): update @oclif/command to version 1.5.20

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 17:06:29 -06:00
greenkeeper[bot] 1ac4a39cd2
Update @oclif/config to the latest version 🚀 (#103)
* fix(package): update @oclif/config to version 1.15.0

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-05-08 14:12:44 -06:00
greenkeeper[bot] 63c0d624df
Update np to the latest version 🚀 (#102)
* chore(package): update np to version 6.2.1

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
2020-05-08 13:42:14 -06:00
greenkeeper[bot] 0483417465
Update jest-watch-typeahead to the latest version 🚀 (#101)
* chore(package): update jest-watch-typeahead to version 0.5.0

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-04-01 18:23:03 -06:00
dependabot[bot] 87316d4f3f
Bump acorn from 5.7.3 to 5.7.4 (#100)
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-04-01 18:19:24 -06:00
greenkeeper[bot] 5ae23a6882
Update request to the latest version 🚀 (#99)
* fix(package): update request to version 2.88.2

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-04-01 18:13:15 -06:00
greenkeeper[bot] 1cfbfd48f7
Update np to the latest version 🚀 (#98)
* chore(package): update np to version 6.0.0

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-04-01 18:06:54 -06:00
greenkeeper[bot] a1b342fd3f
Update @oclif/config to the latest version 🚀 (#97)
* fix(package): update @oclif/config to version 1.14.0

* chore(package): update lockfile package-lock.json

Co-authored-by: greenkeeper[bot] <23040076+greenkeeper[bot]@users.noreply.github.com>
2020-04-01 18:04:47 -06:00
greenkeeper[bot] cbcf513c86 Update @oclif/plugin-help to the latest version 🚀 (#96)
* fix(package): update @oclif/plugin-help to version 2.2.2

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:44:33 -06:00
greenkeeper[bot] c92da8d251 Update jest-watch-typeahead to the latest version 🚀 (#95)
* chore(package): update jest-watch-typeahead to version 0.4.1

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:41:26 -06:00
greenkeeper[bot] 5fd0bf6944 Update strip-ansi to the latest version 🚀 (#94)
* chore(package): update strip-ansi to version 6.0.0

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:37:53 -06:00
greenkeeper[bot] 9a8d02562f Update request-promise-native to the latest version 🚀 (#93)
* fix(package): update request-promise-native to version 1.0.8

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:36:25 -06:00
greenkeeper[bot] 5ae7f88a27 Update dotenv to the latest version 🚀 (#92)
* fix(package): update dotenv to version 8.2.0

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:35:06 -06:00
greenkeeper[bot] 7f5c9f8fab Update np to the latest version 🚀 (#91)
* chore(package): update np to version 5.1.1

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:33:06 -06:00
greenkeeper[bot] 455b7676a6 Update cli-color to the latest version 🚀 (#90)
* fix(package): update cli-color to version 2.0.0

* chore(package): update lockfile package-lock.json

Co-authored-by: David Díaz <seich@martianwabbit.com>
2020-01-24 17:27:35 -06:00
greenkeeper[bot] 19b9bb85cc Update deepmerge to the latest version 🚀 (#89)
* fix(package): update deepmerge to version 4.1.1

* chore(package): update lockfile package-lock.json
2020-01-24 17:19:45 -06:00
David Diaz 4f6078758d 0.10.1 2019-10-07 21:03:15 -06:00
David Díaz ee71f16e56
Dependency updates (#88)
* Updated OCLIF dependencies.

* Updated all dependencies.
2019-10-07 20:38:43 -06:00
dependabot[bot] 3e9034a556 Bump mixin-deep from 1.3.1 to 1.3.2 (#85)
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-07 20:19:48 -06:00
greenkeeper[bot] cb43578d2d Update @oclif/config to the latest version 🚀 (#71)
* fix(package): update @oclif/config to version 1.13.0

* chore(package): update lockfile package-lock.json
2019-06-01 13:39:44 -06:00
greenkeeper[bot] 1bc2e4cbd4 Update jest-watch-typeahead to the latest version 🚀 (#70)
* chore(package): update jest-watch-typeahead to version 0.3.1

* chore(package): update lockfile package-lock.json
2019-06-01 11:32:17 -06:00
David Díaz 3e73ec48db
Greenkeeper/@oclif/config 1.12.12 (#69)
* fix(package): update @oclif/config to version 1.12.12

Closes #61

* chore(package): update lockfile package-lock.json
2019-05-03 22:11:12 -06:00
greenkeeper[bot] c5bf148d74 Update @oclif/command to the latest version 🚀 (#62)
* fix(package): update @oclif/command to version 1.5.12

* chore(package): update lockfile package-lock.json
2019-05-03 17:32:08 -06:00
greenkeeper[bot] 209ceccc36 Update dotenv to the latest version 🚀 (#60)
* fix(package): update dotenv to version 7.0.0

* chore(package): update lockfile package-lock.json
2019-05-03 17:06:21 -06:00
greenkeeper[bot] 0a0d2e258b Update strip-ansi to the latest version 🚀 (#63)
* chore(package): update strip-ansi to version 5.2.0

* chore(package): update lockfile package-lock.json
2019-05-03 17:00:25 -06:00
greenkeeper[bot] b4355c0750 Update js-yaml to the latest version 🚀 (#64)
* fix(package): update js-yaml to version 3.13.0

* chore(package): update lockfile package-lock.json
2019-05-03 15:07:20 -06:00
greenkeeper[bot] 54bafbb4a5 fix(package): update globby to version 9.2.0 (#65) 2019-05-03 14:58:54 -06:00
greenkeeper[bot] c0f28fc5d0 Update jest-watch-typeahead to the latest version 🚀 (#66)
* chore(package): update jest-watch-typeahead to version 0.3.0

* chore(package): update lockfile package-lock.json
2019-05-03 14:56:14 -06:00
greenkeeper[bot] 1e67cffebf Update np to the latest version 🚀 (#67)
* chore(package): update np to version 5.0.0

* chore(package): update lockfile package-lock.json
2019-05-03 14:49:58 -06:00
greenkeeper[bot] 4acb27174d Update is-plain-object to the latest version 🚀 (#68)
* fix(package): update is-plain-object to version 3.0.0

* chore(package): update lockfile package-lock.json
2019-05-03 14:48:43 -06:00
David Diaz 2610ce5a0d 0.10.0 2019-03-12 15:33:02 -06:00
David Diaz 7854333a50 Run tests in band. 2019-03-12 11:21:11 -06:00
Sergio Díaz f7cb19276b
Updating circleci config to version 2. (#59) 2019-03-12 11:14:23 -06:00
Sergio Díaz b4e27b179b
Updated jest. (#56) 2019-03-11 22:23:12 -06:00
Sergio Díaz fea72dde2a
Allow urls as endpoints (#55)
* Added support for multiple request configurations.

This allows multiple requests to hit the same verb+path combination.
This was previously impossible, to address it, it's now possible to pass
an array of request settings as the body of a request each request will
be added to the request list individually.

Example:
```
GET /some/path:
  - alias: first
    headers:
      request: first
  - alias: second
    headers:
      request: second
```

* Allows using full urls in the request key

This allows you to break out of the top-level endpoint. If you send a
full url instead of a path it'll be used instead of the top level
endpoint.

Example:
```
endpoint: http://example.com
GET /posts: get-posts
POST http://api.example.com/posts: post-post
```
2019-03-11 22:04:20 -06:00
Sergio Díaz 94332bd125
Added support for multiple request configurations. (#54)
This allows multiple requests to hit the same verb+path combination.
This was previously impossible, to address it, it's now possible to pass
an array of request settings as the body of a request each request will
be added to the request list individually.

Example:
```
GET /some/path:
  - alias: first
    headers:
      request: first
  - alias: second
    headers:
      request: second
```
2019-03-11 21:14:34 -06:00
greenkeeper[bot] 8a1ecc67bd Update strip-ansi to the latest version 🚀 (#53)
* chore(package): update strip-ansi to version 5.1.0

* chore(package): update lockfile package-lock.json
2019-03-11 17:35:31 -06:00
greenkeeper[bot] 2961b1ea54 Update globby to the latest version 🚀 (#51)
* fix(package): update globby to version 9.1.0

* chore(package): update lockfile package-lock.json
2019-03-06 11:19:35 -06:00
Sergio Díaz 8b6bbfd77b
Greenkeeper/@oclif/config 1.12.9 (#52)
* chore(package): update @oclif/config to version 1.12.9

* chore(package): update lockfile package-lock.json
2019-03-06 11:16:46 -06:00
Sergio Díaz f0298a973b
Updated all dependencies. (#50)
I also used this opportunity to pin dependencies to specific package versions instead of ranges. Should be nicer in the long run.
2018-12-13 14:19:18 -06:00
greenkeeper[bot] 8c453e6974 Update deepmerge to the latest version 🚀 (#49)
* fix(package): update deepmerge to version 3.0.0

* chore(package): update lockfile package-lock.json
2018-12-07 09:58:46 -06:00
greenkeeper[bot] 865d7976fe Update joi to the latest version 🚀 (#47)
* fix(package): update joi to version 14.0.0

* chore(package): update lockfile package-lock.json
2018-12-07 09:43:02 -06:00
greenkeeper[bot] 379395bcbd Update strip-ansi to the latest version 🚀 (#46)
* chore(package): update strip-ansi to version 5.0.0

* chore(package): update lockfile package-lock.json
2018-12-07 09:36:34 -06:00
greenkeeper[bot] d517d4055e fix(package): update requireg to version 0.2.0 (#45) 2018-12-07 09:33:35 -06:00
Sergio Díaz d5da7d87e2
Small refactoring. (#44)
Basically I like how when using Object.entries you can destructure and
name the value. It feels a lot more explicit to give the value a name
rather than refer to it as obj[k]. The syntax is not super pretty though.
2018-08-05 00:00:54 -06:00
Sergio Díaz f2b9cad662
Updated Readme. (#43) 2018-08-01 11:14:20 -06:00
David Diaz cc805d4adb 0.9.5 2018-08-01 00:35:51 -06:00
Sergio Díaz 1814a00835
Updated package.json (#42) 2018-08-01 00:27:09 -06:00
Sergio Díaz 24c0bfb04f
Added np for automating releases. (#41) 2018-08-01 00:04:10 -06:00
Sergio Díaz 52e88a3703
This allows extra documents in a yaml config file to be embeded as hosts.(#40)
This allows extra documents in a yaml config file to be embeded as hosts.
2018-07-31 23:23:32 -06:00
Sergio Díaz 8849fe1e90
Updated dependencies. (#39) 2018-07-18 15:00:14 -06:00
David Diaz 6be3ce05c2 0.9.4 2018-06-19 11:37:26 -06:00
David Diaz b6c5a0b01e Added beau-std as a direct dependency of Beau. 2018-06-19 11:29:41 -06:00
Sergio Díaz 14704c09d1
Added a version mismatch warning. (#37)
* I need to stop rewriting builtin methods with reduce.

* Removed redundant fields from the config class.

* Added a warning when module version and beau.yml version differ.
2018-06-14 23:12:16 -06:00
Sergio Díaz 450d53e9f9
I need to stop rewriting builtin methods with reduce. (#36) 2018-06-14 19:27:03 -06:00
Sergio Díaz ef34ea04aa
Adding basic tests to shared utilities. (#35) 2018-06-06 16:56:12 -06:00
Sergio Díaz cc7245b501
Cleaning up tests. (#34) 2018-06-02 23:47:03 -06:00
Sergio Díaz 8e961211a5
Updated dependencies. (#32) 2018-05-29 15:34:37 -06:00
Sergio Díaz c8ff4945d6
Updated tests. (#30)
With the introduction of data-driven tests in Jest 23 CLI tests could be made significantly smaller and easier to keep updated.
2018-05-29 15:23:58 -06:00
David Diaz 8fcd8b9fd3 Updated README. 2018-05-24 00:20:51 -06:00
Sergio Díaz 26b33fbf00
Added tests to all CLI commands. (#28)
* Moved the spinner initialization to the base class.

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

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

* Added a spec file for the ListCommand.

* Added tests for all CLI commands.

* Update some old tests.

Added missing cases and tests.

Most of these are kind useless but I hope I won't have to touch them
again.
2018-05-22 21:42:52 -06:00
Sergio Díaz 23064040df
Stopped iterating over keys and started assigning instead. (#27)
I don't actually remember why I did it this way. I probably wanted to be
a lot more strict with the keys originally which seems silly in
retrospective.
2018-05-22 12:55:58 -06:00
Sergio Díaz dbc7addb39
Added flags for quiet and as-json output. (#26)
--as-json outputs the response as json, this allows you to use tools
like jq on the output which is nice.

--quiet makes it so no output is printed. I added this in case you are
looping Beau and don't want to see the output of every single request.
Example: `seq 10 | xargs -I{} beau request hi --quiet`.
2018-05-22 12:08:32 -06:00
David Diaz 293dde59c4 0.9.3 2018-05-17 10:54:15 -06:00
Sergio Díaz a9ba5b0bb7
: is valid as a variable when talking about hosts. (#25) 2018-05-17 10:52:35 -06:00
David Diaz c0923dcce5 0.9.2 2018-05-17 10:02:16 -06:00
Sergio Díaz 29940d2a24
This fixes an issue where / and \ and : where considered valid names for (#24)
replacements.
2018-05-17 10:01:02 -06:00
Sergio Díaz aba33e7fc3
Fix: Env variables (#23)
I am making a couple of small changes to how external environment
variables are used. From now own everything comming from the outside,
namely the .env file and the params flag are assigned to the '_'
variable within the internal environment. This is mostly to namespace
things and make it clear where they are coming from.
2018-05-17 09:58:44 -06:00
David Diaz d2eabfb19e 0.9.1 2018-05-16 23:13:42 -06:00
Sergio Díaz 0677497074
Making improvements to the plugins system. (#22)
This is mostly some clean up. Now plugins are checked for existence
before being required. The regex for dynamic values was improved a
little in an attemp to make it more reliable. Plugins are only required
once now.

The CLI's CWD is set to that of the config file. This makes relative
paths mentioned in the file more reliable.
2018-05-16 18:24:52 -06:00
David Diaz 9ba3027017 Added swp to gitignore. 2018-05-07 17:18:03 +00:00
54 changed files with 8973 additions and 6585 deletions

1
.gitattributes vendored
View File

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

37
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,37 @@
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,2 +1,3 @@
node_modules/ node_modules/
coverage/ coverage/
*.swp

View File

@ -5,7 +5,6 @@ useTabs: false
trailingComma: none trailingComma: none
bracketSpacing: true bracketSpacing: true
jsxBracketSameLine: true jsxBracketSameLine: true
parser: babylon semi: false
semi: true
requirePragma: false requirePragma: false
proseWrap: always proseWrap: always

42
LICENSE
View File

@ -1,7 +1,41 @@
Copyright 2018 David Sergio Díaz “Commons Clause” License Condition v1.0
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 Software is provided to you by the Licensor under the License, as defined
below, subject to the following condition.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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 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. 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.

View File

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

30
bin/cli/__mocks__/base.js Normal file
View File

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

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

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

View File

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

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

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

@ -0,0 +1,37 @@
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,28 +1,61 @@
const yaml = require('js-yaml'); const { Command, flags } = require('@oclif/command')
const fs = require('fs'); const yaml = require('js-yaml')
const dotenv = require('dotenv'); const fs = require('fs')
const { Command, flags } = require('@oclif/command'); 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 Beau = require('../../src/beau'); const schema = require('../../schema.json')
const ajv = new Ajv()
const validate = ajv.compile(schema)
class Base extends Command { class Base extends Command {
openConfigFile(configFile) { openConfigFile(configFile) {
if (!fs.existsSync(configFile)) { if (!fs.existsSync(configFile)) {
this.error(`The config file, ${configFile} was not found.`); throw new Error(`The config file, ${configFile} was not found.`)
this.exit(1);
} }
return yaml.safeLoad(fs.readFileSync(configFile, 'utf-8')); 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 = []) { loadConfig(configFile, params = []) {
const config = this.openConfigFile(configFile); const config = this.openConfigFile(configFile)
const env = dotenv.config().parsed || {}; const env = dotenv.config().parsed || {}
params = dotenv.parse(params.reduce((a, p) => a + '\n' + p, '')); params = dotenv.parse(params.join('\n'))
const envParams = { params: Object.assign(env, params) }; const envParams = { _: Object.assign(env, params) }
return new Beau(config, envParams); const configFileDir = path.dirname(
path.resolve(process.cwd(), configFile)
)
process.chdir(configFileDir)
return new Beau(config, envParams)
} }
} }
@ -34,11 +67,11 @@ Base.flags = {
}), }),
verbose: flags.boolean({ verbose: flags.boolean({
char: 'V', char: 'V',
description: 'Show all additional information available for a command.' description: `Show all additional information available for a command.`
}), }),
'no-format': flags.boolean({ 'no-format': flags.boolean({
description: `Disables color formatting for usage on external tools.` description: `Disables color formatting for usage on external tools.`
}) })
}; }
module.exports = Base; module.exports = Base

View File

@ -1,27 +1,23 @@
const clc = require('cli-color'); const clc = require('cli-color')
const { Line } = require('clui'); const { Line } = require('clui')
const { flags } = require('@oclif/command'); const { expandPath } = require('../../../src/shared')
const Base = require('../base')
const Base = require('../base');
class ListCommand extends Base { class ListCommand extends Base {
async run() { async run() {
const { flags } = this.parse(ListCommand); const { flags } = this.parse(ListCommand)
const Beau = this.loadConfig(flags.config); const Beau = this.loadConfig(flags.config)
if (flags['no-format']) { if (flags['no-format']) {
return Beau.requests.list.forEach( return Beau.requests.list.forEach(
({ VERB, ALIAS, ENDPOINT, PATH }) => ({ VERB, ALIAS, ENDPOINT, PATH }) =>
this.log( this.log(
VERB + `${VERB}\t${ALIAS}\t${ENDPOINT.replace(
`\t` + /\/$/,
ALIAS + ''
`\t` + )}/${PATH.replace(/^\//, '')}`
ENDPOINT.replace(/\/$/, '') +
`/` +
PATH.replace(/^\//, '')
) )
); )
} }
new Line() new Line()
@ -29,24 +25,22 @@ class ListCommand extends Base {
.column('HTTP Verb', 20, [clc.cyan]) .column('HTTP Verb', 20, [clc.cyan])
.column('Alias', 30, [clc.cyan]) .column('Alias', 30, [clc.cyan])
.column('Endpoint', 20, [clc.cyan]) .column('Endpoint', 20, [clc.cyan])
.output(); .output()
Beau.requests.list.forEach(({ VERB, ALIAS, ENDPOINT, PATH }) => Beau.requests.list.forEach(({ VERB, ALIAS, ENDPOINT, PATH }) =>
new Line() new Line()
.padding(2) .padding(2)
.column(VERB, 20, [clc.yellow]) .column(VERB, 20, [clc.yellow])
.column(ALIAS, 30, [clc.yellow]) .column(ALIAS, 30, [clc.yellow])
.column( .column(expandPath(ENDPOINT, PATH))
ENDPOINT.replace(/\/$/, '') + '/' + PATH.replace(/^\//, '')
)
.output() .output()
); )
new Line().output(); new Line().output()
} }
} }
ListCommand.description = `Lists all available requests in the config file.`; ListCommand.description = `Lists all available requests in the config file.`
ListCommand.flags = { ...Base.flags }; ListCommand.flags = { ...Base.flags }
module.exports = ListCommand; module.exports = ListCommand

View File

@ -1,35 +1,41 @@
const clc = require('cli-color'); const Base = require('../base')
const jsome = require('jsome'); const cj = require('color-json')
const { Line, Spinner } = require('clui'); const clc = require('cli-color')
const { flags } = require('@oclif/command'); const prompts = require('prompts')
const { Line, Spinner } = require('clui')
const Base = require('../base'); const { flags } = require('@oclif/command')
const { expandPath } = require('../../../src/shared')
class RequestCommand extends Base { class RequestCommand extends Base {
prettyOutput(res, verbose = false) { prettyOutput(res, verbose = false) {
let { status, body } = res.response; let { status, body } = res.response
this.spinner.stop(); this.spinner.stop()
status = status.toString().startsWith(2) status = status.toString().startsWith(2)
? clc.green(status) ? clc.green(status)
: clc.red(status); : clc.red(status)
new Line() new Line()
.padding(2) .padding(2)
.column('Status', 20, [clc.cyan]) .column('Status', 20, [clc.cyan])
.column('Endpoint', 20, [clc.cyan]) .column('Endpoint', 20, [clc.cyan])
.output(); .output()
new Line() new Line()
.padding(2) .padding(2)
.column(status, 20) .column(status, 20)
.column(res.request.endpoint) .column(res.request.endpoint)
.output(); .output()
new Line().output(); new Line().output()
jsome((verbose ? res : body) || null); const result = (verbose ? res : body) || null
if (typeof result === 'object') {
this.log(cj(result))
} else if (typeof result === 'string') {
this.log(result)
}
} }
async run() { async run() {
@ -38,43 +44,84 @@ class RequestCommand extends Base {
param: params, param: params,
config, config,
'no-format': noFormat = false, 'no-format': noFormat = false,
verbose = false verbose = false,
'as-json': asJson = false,
quiet = false,
interactive = false
}, },
args args
} = this.parse(RequestCommand); } = this.parse(RequestCommand)
const Beau = this.loadConfig(config, params); const Beau = this.loadConfig(config, params)
const spinnerSprite = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']; const spinnerSprite = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']
this.spinner = new Spinner( this.spinner = new Spinner('', spinnerSprite)
clc.yellow(`Requesting: ${args.alias}`),
spinnerSprite
);
try { let spinnerEnabled = !noFormat && !asJson && !quiet
if (!noFormat) {
this.spinner.start();
}
let res = await Beau.requests.execByAlias(args.alias); if (typeof args.alias == 'undefined' && !interactive) {
this.error(
if (noFormat) { 'Missing 1 required argument: The alias of the request to execute.'
this.log(res.response.status); )
this.log(res.request.endpoint);
this.log(JSON.stringify(res.response.headers));
this.log(JSON.stringify(res.response.body));
} else {
this.prettyOutput(res, verbose);
}
} catch (err) {
new Line().output();
this.spinner.stop();
this.error(err.message);
} }
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.description = `Executes a request by name.`
RequestCommand.flags = { RequestCommand.flags = {
...Base.flags, ...Base.flags,
param: flags.string({ param: flags.string({
@ -82,15 +129,30 @@ RequestCommand.flags = {
multiple: true, multiple: true,
default: [], default: [],
description: `Allows you to inject values into the request's environment.` 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 = [ RequestCommand.args = [
{ {
name: 'alias', name: 'alias',
required: true, required: false,
description: `The alias of the request to execute.` description: `The alias of the request to execute.`
} }
]; ]
module.exports = RequestCommand; module.exports = RequestCommand

View File

@ -1,34 +0,0 @@
const clc = require('cli-color');
const fs = require('fs');
const yaml = require('js-yaml');
const { flags } = require('@oclif/command');
const Base = require('../base');
const { validate } = require('../../../src/schema.js');
class ValidateCommand extends Base {
async run() {
const { flags, args } = this.parse(ValidateCommand);
const configFile = args.alias || flags.config;
const config = this.openConfigFile(configFile);
let result = await validate(config);
if (result.valid) {
this.log(`${configFile} is valid.`);
} else {
this.error(result.message);
}
}
}
ValidateCommand.description = `Validates the given configuration file against Beau's configuration schema.`;
ValidateCommand.flags = { ...Base.flags };
ValidateCommand.args = [
{
name: 'alias',
required: false,
description: `The configuration file to validate.`
}
];
module.exports = ValidateCommand;

View File

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

37
examples/beau.yml Normal file
View File

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

View File

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

View File

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

@ -1,27 +0,0 @@
version: 1
endpoint: https://httpbin.org/
cookiejar: true
environment:
params:
name: David
GET /anything:
alias: anything
payload:
name: $env.params.name
GET /cookies/set:
alias: set-cookies
params:
hello: World
GET /status/418:
alias: teapot
POST /post:
alias: post
formdata:
file: $[createReadStream('./github.yml')]

View File

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

View File

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

View File

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

BIN
media/beau.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
media/usage.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

12828
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,46 @@
{ {
"name": "beau", "name": "beau",
"version": "0.9.0", "version": "0.11.3",
"description": "Testing APIs made easy.", "description": "Testing APIs made easy.",
"main": "./src/beau.js", "main": "./src/beau.js",
"author": "Sergio Diaz <seich@martianwabbit.com>", "author": "David Díaz <seich@martianwabbit.com>",
"license": "MIT", "license": "MIT + Commons Clause",
"scripts": { "scripts": {
"test": "jest", "test": "jest -i",
"test:coverage": "jest --coverage" "test:coverage": "jest --coverage ./src",
"release": "np"
}, },
"files": [
"/src",
"/bin"
],
"dependencies": { "dependencies": {
"@oclif/command": "^1.4.16", "@oclif/command": "1.8.0",
"@oclif/config": "^1.6.13", "@oclif/config": "1.17.0",
"@oclif/plugin-help": "^1.2.5", "@oclif/plugin-help": "3.2.1",
"@oclif/plugin-warn-if-update-available": "^1.3.6", "@oclif/plugin-warn-if-update-available": "1.7.0",
"cli-color": "^1.1.0", "ajv": "7.0.3",
"clui": "^0.3.1", "beau-std": "0.9.4",
"deepmerge": "^2.1.0", "better-ajv-errors": "0.7.0",
"dotenv": "^5.0.1", "cli-color": "2.0.0",
"joi": "^13.2.0", "clui": "0.3.6",
"globby": "^8.0.1", "color-json": "2.0.1",
"is-plain-object": "^2.0.4", "deepmerge": "4.2.2",
"js-yaml": "^3.11.0", "dotenv": "8.2.0",
"jsome": "^2.5.0", "globby": "11.0.2",
"request": "^2.85.0", "is-plain-object": "5.0.0",
"request-promise-native": "^1.0.5", "js-yaml": "4.0.0",
"requireg": "^0.1.6" "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", "repository": "git@github.com:Seich/Beau.git",
"devDependencies": { "devDependencies": {
"jest": "^22.4.0" "jest": "26.6.3",
"jest-watch-typeahead": "0.6.1",
"strip-ansi": "6.0.0",
"np": "7.2.0"
}, },
"oclif": { "oclif": {
"commands": "./bin/cli/commands", "commands": "./bin/cli/commands",
@ -41,12 +52,16 @@
}, },
"jest": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",
"notify": true "notify": true,
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
}, },
"bin": { "bin": {
"beau": "./bin/beau" "beau": "./bin/beau"
}, },
"engines": { "engines": {
"node": ">=8.9.3" "node": ">=8.10.0"
} }
} }

122
schema.json Normal file
View File

@ -0,0 +1,122 @@
{
"$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 +1,11 @@
class DynamicValues { class DynamicValues {
constructor(registry, settings = {}) { constructor(registry, settings = {}) {
registry.defineDynamicValue('add', this.add); registry.defineDynamicValue('add', this.add)
} }
add(x, y) { add(x, y) {
return x + y; return x + y
} }
} }
module.exports = DynamicValues; module.exports = DynamicValues

View File

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

View File

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

View File

@ -1,6 +1,6 @@
function Request(request) { function Request(request) {
if (Request.fail) { if (Request.fail) {
throw new Error(); throw new Error()
} }
return { return {
@ -14,9 +14,9 @@ function Request(request) {
statusCode: 200, statusCode: 200,
headers: [], headers: [],
body: '{"hello": "world"}' body: '{"hello": "world"}'
}; }
} }
Request.fail = false; Request.fail = false
module.exports = Request; module.exports = Request

View File

@ -1,15 +1,15 @@
function requireg(name) { function requireg(name) {
return require(name); return require(name)
} }
requireg.std_resolving = false; requireg.resolving = true
requireg.resolve = function(name) { requireg.resolve = function (name) {
if (requireg.std_resolving) { if (requireg.resolving) {
return ''; return ''
} else { } else {
throw new Error(`Failed to resolve.`); return undefined
} }
}; }
module.exports = requireg; module.exports = requireg

4
src/__mocks__/shared.js Normal file
View File

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

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Beau's config Loader. should create a request list 1`] = ` exports[`Beau's config Loader. should load the config 1`] = `
Beau { Beau {
"config": Config { "config": Config {
"COOKIEJAR": false, "COOKIEJAR": false,
@ -9,10 +9,13 @@ Beau {
"authentication": "hello", "authentication": "hello",
}, },
}, },
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://example.com",
"ENVIRONMENT": Object {}, "ENVIRONMENT": Object {},
"HOSTS": Array [], "HOSTS": Array [],
"PLUGINS": Plugins { "PLUGINS": Plugins {
"autoload": Array [
"std",
],
"context": Object {}, "context": Object {},
"registry": Object { "registry": Object {
"dynamicValues": Array [], "dynamicValues": Array [],
@ -20,27 +23,7 @@ Beau {
"preRequestModifiers": Array [], "preRequestModifiers": Array [],
}, },
}, },
"REQUESTS": Array [ "REQUESTS": Array [],
Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
"VERSION": 1, "VERSION": 1,
"configKeys": Array [ "configKeys": Array [
"VERSION", "VERSION",
@ -51,70 +34,44 @@ Beau {
"HOSTS", "HOSTS",
"COOKIEJAR", "COOKIEJAR",
], ],
"defaultConfigValues": Object {
"COOKIEJAR": 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": RequestList { "requests": RequestList {
"REQUESTS": Array [
Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
"cache": RequestCache { "cache": RequestCache {
"$cache": Object { "$cache": Object {
"env": Object {}, "env": Object {},
}, },
}, },
"config": Config { "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, "COOKIEJAR": false,
"DEFAULTS": Object { "DEPENDENCIES": Set {},
"headers": Object { "ENDPOINT": "http://example.com",
"authentication": "hello", "PATH": "/posts/1",
}, "REQUEST": "GET /posts/1",
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://example.com",
"REQUEST": "GET /posts/1",
}, },
"ENDPOINT": "http://jsonplaceholder.typicode.com", "plugins": Plugins {
"ENVIRONMENT": Object {}, "autoload": Array [
"HOSTS": Array [], "std",
"PLUGINS": Plugins { ],
"context": Object {}, "context": Object {},
"registry": Object { "registry": Object {
"dynamicValues": Array [], "dynamicValues": Array [],
@ -122,133 +79,39 @@ Beau {
"preRequestModifiers": Array [], "preRequestModifiers": Array [],
}, },
}, },
"REQUESTS": Array [
Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
Object {
"ALIAS": "user",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
"hello": "world",
},
"REQUEST": "GET /user",
},
],
"VERSION": 1,
"configKeys": Array [
"VERSION",
"ENDPOINT",
"PLUGINS",
"DEFAULTS",
"ENVIRONMENT",
"HOSTS",
"COOKIEJAR",
],
"defaultConfigValues": Object {
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"doc": Object {
"GET /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,
},
}, },
"list": Array [ Request {
Request { "ALIAS": "user",
"ALIAS": "get-post", "COOKIEJAR": false,
"COOKIEJAR": false, "DEPENDENCIES": Set {},
"DEPENDENCIES": Set {}, "ENDPOINT": "http://example.com",
"ENDPOINT": "http://jsonplaceholder.typicode.com", "HEADERS": Object {
"FORM": undefined, "hello": "world",
"FORMDATA": undefined,
"HEADERS": Object {
"authentication": "hello",
},
"PARAMS": undefined,
"PATH": "/posts/1",
"PAYLOAD": undefined,
"REQUEST": "GET /posts/1",
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "get-post",
"COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"HEADERS": Object {
"authentication": "hello",
},
"REQUEST": "GET /posts/1",
},
"plugins": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
},
}, },
Request { "PATH": "/user",
"REQUEST": "GET /user",
"VERB": "GET",
"originalRequest": Object {
"ALIAS": "user", "ALIAS": "user",
"COOKIEJAR": false, "COOKIEJAR": false,
"DEPENDENCIES": Set {}, "ENDPOINT": "http://example.com",
"ENDPOINT": "http://jsonplaceholder.typicode.com",
"FORM": undefined,
"FORMDATA": undefined,
"HEADERS": Object { "HEADERS": Object {
"authentication": "hello",
"hello": "world", "hello": "world",
}, },
"PARAMS": undefined,
"PATH": "/user",
"PAYLOAD": undefined,
"REQUEST": "GET /user", "REQUEST": "GET /user",
"VERB": "GET", },
"originalRequest": Object { "plugins": Plugins {
"ALIAS": "user", "autoload": Array [
"COOKIEJAR": false, "std",
"ENDPOINT": "http://jsonplaceholder.typicode.com", ],
"HEADERS": Object { "context": Object {},
"authentication": "hello", "registry": Object {
"hello": "world", "dynamicValues": Array [],
}, "postRequestModifiers": Array [],
"REQUEST": "GET /user", "preRequestModifiers": Array [],
},
"plugins": Plugins {
"context": Object {},
"registry": Object {
"dynamicValues": Array [],
"postRequestModifiers": Array [],
"preRequestModifiers": Array [],
},
}, },
}, },
], },
}, ],
} }
`; `;

View File

@ -42,6 +42,9 @@ Config {
}, },
], ],
"PLUGINS": Plugins { "PLUGINS": Plugins {
"autoload": Array [
"std",
],
"context": Object {}, "context": Object {},
"registry": Object { "registry": Object {
"dynamicValues": Array [], "dynamicValues": Array [],
@ -119,55 +122,6 @@ Config {
"HOSTS", "HOSTS",
"COOKIEJAR", "COOKIEJAR",
], ],
"defaultConfigValues": Object {
"COOKIEJAR": false,
"DEFAULTS": Object {},
"ENDPOINT": "",
"ENVIRONMENT": Object {},
"HOSTS": Array [],
"PLUGINS": Array [],
"VERSION": 1,
},
"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",
},
],
},
} }
`; `;
@ -179,10 +133,13 @@ Config {
"authentication": "hello", "authentication": "hello",
}, },
}, },
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://example.com",
"ENVIRONMENT": Object {}, "ENVIRONMENT": Object {},
"HOSTS": Array [], "HOSTS": Array [],
"PLUGINS": Plugins { "PLUGINS": Plugins {
"autoload": Array [
"std",
],
"context": Object {}, "context": Object {},
"registry": Object { "registry": Object {
"dynamicValues": Array [], "dynamicValues": Array [],
@ -194,7 +151,7 @@ Config {
Object { Object {
"ALIAS": "get-post", "ALIAS": "get-post",
"COOKIEJAR": false, "COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://example.com",
"HEADERS": Object { "HEADERS": Object {
"authentication": "hello", "authentication": "hello",
}, },
@ -203,7 +160,7 @@ Config {
Object { Object {
"ALIAS": "user", "ALIAS": "user",
"COOKIEJAR": false, "COOKIEJAR": false,
"ENDPOINT": "http://jsonplaceholder.typicode.com", "ENDPOINT": "http://example.com",
"HEADERS": Object { "HEADERS": Object {
"authentication": "hello", "authentication": "hello",
"hello": "world", "hello": "world",
@ -221,30 +178,5 @@ Config {
"HOSTS", "HOSTS",
"COOKIEJAR", "COOKIEJAR",
], ],
"defaultConfigValues": Object {
"COOKIEJAR": 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,
},
} }
`; `;

View File

@ -20,6 +20,7 @@ Object {
exports[`Beau's plugin system shouldn't do anything when given an empty array. 1`] = ` exports[`Beau's plugin system shouldn't do anything when given an empty array. 1`] = `
Plugins { Plugins {
"autoload": Array [],
"context": Object {}, "context": Object {},
"registry": Object { "registry": Object {
"dynamicValues": Array [], "dynamicValues": Array [],

View File

@ -7,7 +7,7 @@ Object {
"body": Object { "body": Object {
"username": "seich", "username": "seich",
}, },
"endpoint": "http://martianwabbit.com/user", "endpoint": "http://example.com/user",
"headers": Object { "headers": Object {
"authentication": "BEARER abc123", "authentication": "BEARER abc123",
}, },
@ -25,7 +25,7 @@ Object {
"body": "{\\"hello\\": \\"world\\"}", "body": "{\\"hello\\": \\"world\\"}",
"request": Object { "request": Object {
"body": undefined, "body": undefined,
"endpoint": "http://martianwabbit.com/user", "endpoint": "http://example.com/user",
"headers": undefined, "headers": undefined,
}, },
"response": Object { "response": Object {

View File

@ -1,26 +1,59 @@
const yaml = require('js-yaml'); const yaml = require('js-yaml')
const Beau = require('../beau'); const Beau = require('../beau')
const { moduleVersion } = require('../shared')
jest.mock('../shared')
const requireg = require('requireg')
requireg.resolving = false
describe(`Beau's config Loader.`, () => { describe(`Beau's config Loader.`, () => {
it('should create a request list', () => { it('should load the config', () => {
const doc = yaml.safeLoad(` moduleVersion.mockReturnValue(1)
const doc = yaml.load(`
version: 1 version: 1
endpoint: 'http://jsonplaceholder.typicode.com' endpoint: 'http://example.com'
defaults: defaults:
headers: headers:
authentication: hello 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 /posts/1: get-post
GET /user: GET /user:
alias: user alias: user
headers: headers:
hello: world hello: world
`); `)
const beau = new Beau(doc); const beau = new Beau(doc)
expect(beau.requests).toMatchSnapshot()
})
expect(beau.requests).toBeDefined(); it('should display a warning if the module version and the beau file version are different', () => {
expect(beau).toMatchSnapshot(); 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()
})
})

View File

@ -1,22 +1,25 @@
const yaml = require('js-yaml'); const yaml = require('js-yaml')
const Config = require('../config'); const Config = require('../config')
const requireg = require('requireg')
requireg.resolving = false
describe('Config', () => { describe('Config', () => {
it('should load valid config keys', () => { it('should load valid config keys', () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
version: 1 version: 1
endpoint: http://martianwabbit.com endpoint: http://martianwabbit.com
shouldntBeAdded: true shouldntBeAdded: true
`); `)
const config = new Config(doc); const config = new Config(doc)
expect(config.ENDPOINT).toBe(doc.endpoint); expect(config.ENDPOINT).toBe(doc.endpoint)
expect(config.VERSION).toBe(doc.version); expect(config.VERSION).toBe(doc.version)
expect(config.shouldntBeAdded).toBeUndefined(); expect(config.shouldntBeAdded).toBeUndefined()
}); })
it('should load requests', () => { it('should load requests', () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
endpoint: http://example.com endpoint: http://example.com
GET /profile: get-profile GET /profile: get-profile
@ -26,16 +29,16 @@ describe('Config', () => {
alias: user alias: user
headers: headers:
hello: world hello: world
`); `)
const config = new Config(doc); const config = new Config(doc)
expect(Object.keys(config.REQUESTS).length).toBe(4); expect(Object.keys(config.REQUESTS).length).toBe(4)
}); })
it('should set up defaults for all requests', () => { it('should set up defaults for all requests', () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
version: 1 version: 1
endpoint: 'http://jsonplaceholder.typicode.com' endpoint: 'http://example.com'
defaults: defaults:
HEADERS: HEADERS:
@ -46,18 +49,19 @@ describe('Config', () => {
alias: user alias: user
headers: headers:
hello: world hello: world
`); `)
const config = new Config(doc); const config = new Config(doc)
expect(config).toMatchSnapshot(); expect(config).toMatchSnapshot()
Object.values(config.REQUESTS).forEach(r => { Object.values(config.REQUESTS).forEach((r) => {
expect(r.HEADERS.authentication).toMatch('hello'); expect(r.HEADERS.authentication).toMatch('hello')
}); })
}); })
it('should load multiple hosts', () => { it('should load multiple hosts', () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
version: 1
endpoint: http://example.org endpoint: http://example.org
defaults: defaults:
@ -93,15 +97,15 @@ describe('Config', () => {
endpoint: http://example.info endpoint: http://example.info
GET /posts: posts 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', () => { it('should namespace all aliases within an host', () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
hosts: hosts:
- host: test1 - host: test1
endpoint: http://example.com endpoint: http://example.com
@ -109,16 +113,16 @@ describe('Config', () => {
- host: test2 - host: test2
endpoint: http://example.net endpoint: http://example.net
GET /posts: posts GET /posts: posts
`); `)
let config = new Config(doc); let config = new Config(doc)
expect(config.REQUESTS[0].ALIAS).toBe('test1:posts'); expect(config.REQUESTS[0].ALIAS).toBe('test1:posts')
expect(config.REQUESTS[1].ALIAS).toBe('test2:posts'); expect(config.REQUESTS[1].ALIAS).toBe('test2:posts')
}); })
it(`should throw if host doesn't have a host key`, () => { it(`should throw if host doesn't have a host key`, () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
hosts: hosts:
- endpoint: http://example.com - endpoint: http://example.com
GET /posts: posts GET /posts: posts
@ -126,13 +130,13 @@ describe('Config', () => {
- host: test2 - host: test2
endpoint: http://example.net endpoint: http://example.net
GET /posts: posts GET /posts: posts
`); `)
expect(() => new Config(doc)).toThrow(); expect(() => new Config(doc)).toThrow()
}); })
it(`should merge host settings with global settings`, () => { it(`should merge host settings with global settings`, () => {
const doc = yaml.safeLoad(` const doc = yaml.load(`
defaults: defaults:
headers: headers:
hello: 1 hello: 1
@ -148,9 +152,25 @@ describe('Config', () => {
headers: false headers: false
GET /posts: posts GET /posts: posts
`); `)
let config = new Config(doc); let config = new Config(doc)
expect(config.REQUESTS[0].HEADERS.hello).toBe(1); 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)
})
})

View File

@ -1,53 +1,41 @@
const yaml = require('js-yaml'); const Plugins = require('../plugins')
const Config = require('../config'); const Request = require('../request')
const Plugins = require('../plugins'); const RequestCache = require('../requestCache')
const Request = require('../request'); const requireg = require('requireg')
const RequestCache = require('../requestCache');
const requireg = require('requireg');
describe(`Beau's plugin system`, () => { describe(`Beau's plugin system`, () => {
let config; let request
let request; let plugins
let plugins;
let doc;
beforeEach(() => { beforeEach(() => {
doc = yaml.safeLoad(` plugins = new Plugins([{ Modifiers: [Object] }, 'DynamicValues'], [])
version: 1 })
endpoint: 'http://example.com'
plugins:
- Modifiers:
data: hi
- DynamicValues
GET /posts/$[add(1, 1)]: get-post
`);
config = new Config(doc);
plugins = config.PLUGINS;
});
it('should load all plugins', () => { it('should load all plugins', () => {
expect(plugins.registry.preRequestModifiers.length).toBe(1); expect(plugins.registry.preRequestModifiers.length).toBe(1)
expect(plugins.registry.postRequestModifiers.length).toBe(1); expect(plugins.registry.postRequestModifiers.length).toBe(1)
expect(plugins.registry.dynamicValues.length).toBe(1); expect(plugins.registry.dynamicValues.length).toBe(1)
}); })
it(`should load autoload plugins`, () => {
requireg.std_resolving = true;
config = new Config(doc);
expect(config.PLUGINS.registry.dynamicValues.length).toBe(2);
requireg.std_resolving = false;
});
it(`should throw if given an invalid configuration`, () => { it(`should throw if given an invalid configuration`, () => {
expect(() => new Plugins([{ test1: true, test2: true }])).toThrow(); expect(() => new Plugins([{ test1: true, test2: true }])).toThrow()
}); })
it(`shouldn't do anything when given an empty array.`, () => { it(`shouldn't do anything when given an empty array.`, () => {
expect(new Plugins()).toMatchSnapshot(); 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`, () => { describe(`Request Modifiers`, () => {
beforeEach(() => { beforeEach(() => {
@ -58,13 +46,13 @@ describe(`Beau's plugin system`, () => {
alias: 'update' alias: 'update'
}, },
plugins plugins
); )
}); })
it(`should modify the request and response using modifiers.`, async () => { it(`should modify the request and response using modifiers.`, async () => {
await expect(request.exec()).resolves.toMatchSnapshot(); await expect(request.exec()).resolves.toMatchSnapshot()
}); })
}); })
describe(`Dynamic Values`, () => { describe(`Dynamic Values`, () => {
beforeEach(() => { beforeEach(() => {
@ -80,34 +68,34 @@ describe(`Beau's plugin system`, () => {
payload: 'counted $[add(1, $value2)] so far.' payload: 'counted $[add(1, $value2)] so far.'
}, },
plugins plugins
); )
}); })
let cache = new RequestCache(); let cache = new RequestCache()
cache.add('value2', '2'); cache.add('value2', '2')
it(`should look for dynamic values executing and replacing them`, async () => { it(`should look for dynamic values executing and replacing them`, async () => {
let req = await request.exec(cache); let req = await request.exec(cache)
expect(req).toHaveProperty('request.body', 'counted 3 so far.'); 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 () => { it(`should change the internal datatype if the only thing in the value is the dynamic value`, async () => {
let req = await request.exec(cache); let req = await request.exec(cache)
expect(req).toHaveProperty('request.headers.count', 3); expect(req).toHaveProperty('request.headers.count', 3)
}); })
it(`should return empty values as empty`, async () => { it(`should return empty values as empty`, async () => {
let req = await request.exec(cache); let req = await request.exec(cache)
expect(req).toHaveProperty('request.headers.empty', ''); expect(req).toHaveProperty('request.headers.empty', '')
}); })
it(`should throw when calling an undefined dynamic value`, async () => { it(`should throw when calling an undefined dynamic value`, async () => {
request = new Request({ request = new Request({
request: 'POST /hello/$[notAvailable(1, 2)]', request: 'POST /hello/$[notAvailable(1, 2)]',
alias: 'say-hello' alias: 'say-hello'
}); })
await expect(request.exec()).rejects.toThrow(); await expect(request.exec()).rejects.toThrow()
}); })
}); })
}); })

View File

@ -1,19 +1,18 @@
const Request = require('../request'); const Request = require('../request')
const RequestCache = require('../requestCache'); const RequestCache = require('../requestCache')
const RequestList = require('../requestList'); const requestPromiseNativeMock = require('request-promise-native')
const requestPromiseNativeMock = require('request-promise-native');
describe('Request', () => { describe('Request', () => {
let cache; let cache
let validRequestConfig; let validRequestConfig
let invalidRequestConfig; let invalidRequestConfig
let request; let request
let requestWithoutDependencies; let requestWithoutDependencies
beforeEach(() => { beforeEach(() => {
validRequestConfig = { validRequestConfig = {
request: 'POST /user', request: 'POST /user',
endpoint: 'http://martianwabbit.com', endpoint: 'http://example.com',
alias: 'update', alias: 'update',
params: { params: {
userId: '$profile.UserId' userId: '$profile.UserId'
@ -24,54 +23,78 @@ describe('Request', () => {
payload: { payload: {
username: 'seich' username: 'seich'
} }
}; }
invalidRequestConfig = { invalidRequestConfig = {
request: `POST /session`, request: `POST /session`,
endpoint: 'http://martianwabbit.com' endpoint: 'http://example.com'
}; }
cache = new RequestCache(); cache = new RequestCache()
cache.add('session', { token: 'abc123' }); cache.add('session', { token: 'abc123' })
cache.add('profile', { UserId: 14 }); cache.add('profile', { UserId: 14 })
request = new Request(validRequestConfig); request = new Request(validRequestConfig)
requestWithoutDependencies = new Request({ requestWithoutDependencies = new Request({
endpoint: 'http://martianwabbit.com', endpoint: 'http://example.com',
request: 'GET /user', request: 'GET /user',
alias: 'show' alias: 'show'
}); })
requestPromiseNativeMock.fail = false; requestPromiseNativeMock.fail = false
}); })
it('should load up the given request', () => { it('should load up the given request', () => {
expect(request.VERB).toBe('POST'); expect(request.VERB).toBe('POST')
expect(request.ENDPOINT).toBe(validRequestConfig.endpoint); expect(request.ENDPOINT).toBe(validRequestConfig.endpoint)
expect(request.HEADERS).toBeDefined(); expect(request.HEADERS).toBeDefined()
expect(request.PAYLOAD).toBeDefined(); expect(request.PAYLOAD).toBeDefined()
expect(request.PARAMS).toBeDefined(); expect(request.PARAMS).toBeDefined()
}); })
it('should throw if a given request is invalid', () => { it('should throw if a given request is invalid', () => {
expect(() => new Request(invalidRequestConfig)).toThrow(); expect(() => new Request(invalidRequestConfig)).toThrow()
}); })
it('should list all of its dependencies', () => { it('should list all of its dependencies', () => {
expect(request.DEPENDENCIES.size).toBe(2); expect(request.DEPENDENCIES.size).toBe(2)
expect(request.DEPENDENCIES).toContain('session'); expect(request.DEPENDENCIES).toContain('session')
expect(request.DEPENDENCIES).toContain('profile'); expect(request.DEPENDENCIES).toContain('profile')
}); })
it('should execute a request', async () => { it('should execute a request', async () => {
await expect(request.exec(cache)).resolves.toMatchSnapshot(); await expect(request.exec(cache)).resolves.toMatchSnapshot()
await expect( await expect(
requestWithoutDependencies.exec() requestWithoutDependencies.exec()
).resolves.toMatchSnapshot(); ).resolves.toMatchSnapshot()
}); })
it('should throw if the request fails', async () => { it('should throw if the request fails', async () => {
requestPromiseNativeMock.fail = true; requestPromiseNativeMock.fail = true
await expect(requestWithoutDependencies.exec()).rejects.toThrow(Error); 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'
})
const requestWithoutPath = new Request({
endpoint: 'http://example.com',
request: 'GET /user',
alias: 'get-user'
})
await expect(requestWithPath.exec()).resolves.toHaveProperty(
'request.endpoint',
'http://martianwabbit.com/user'
)
await expect(requestWithoutPath.exec()).resolves.toHaveProperty(
'request.endpoint',
'http://example.com/user'
)
})
})

View File

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

View File

@ -1,13 +1,13 @@
const Config = require('../config'); const Config = require('../config')
const RequestList = require('../requestList'); const RequestList = require('../requestList')
const requestPromiseNativeMock = require('request-promise-native'); const requestPromiseNativeMock = require('request-promise-native')
describe('RequestList', () => { describe('RequestList', () => {
const endpoint = 'http://martianwabbit.com'; const endpoint = 'http://martianwabbit.com'
let env = { let env = {
environmental: true environmental: true
}; }
const doc = { const doc = {
ENDPOINT: endpoint, ENDPOINT: endpoint,
@ -20,49 +20,49 @@ describe('RequestList', () => {
lastname: 'Diaz' lastname: 'Diaz'
} }
} }
}; }
let requests; let requests
beforeEach(() => { beforeEach(() => {
requestPromiseNativeMock.fail = false; requestPromiseNativeMock.fail = false
let config = new Config(doc); let config = new Config(doc)
requests = new RequestList(config); requests = new RequestList(config)
}); })
it('should allow an empty request list', () => { it('should allow an empty request list', () => {
requests = new RequestList(); requests = new RequestList()
expect(requests.list.length).toBe(0); expect(requests.list.length).toBe(0)
}); })
it('should load valid requests', () => { it('should load valid requests', () => {
expect(requests.list.length).toBe(2); expect(requests.list.length).toBe(2)
}); })
it('should fetch dependencies', async () => { it('should fetch dependencies', async () => {
await expect( await expect(
requests.fetchDependencies(['get-posts']) requests.fetchDependencies(['get-posts'])
).resolves.toMatchSnapshot(); ).resolves.toMatchSnapshot()
}); })
it('should execute requests by alias.', async () => { it('should execute requests by alias.', async () => {
await expect(requests.execByAlias('user')).resolves.toMatchSnapshot(); await expect(requests.execByAlias('user')).resolves.toMatchSnapshot()
}); })
it('should fail if the request fails', async () => { it('should fail if the request fails', async () => {
requestPromiseNativeMock.fail = true; requestPromiseNativeMock.fail = true
await expect(requests.execByAlias('user')).rejects.toThrow(); await expect(requests.execByAlias('user')).rejects.toThrow()
}); })
it('should return a cached result if available', async () => { it('should return a cached result if available', async () => {
const obj = { test: true }; const obj = { test: true }
requests.cache.add('test', obj); requests.cache.add('test', obj)
await expect(requests.execByAlias('test')).resolves.toBe(obj); await expect(requests.execByAlias('test')).resolves.toBe(obj)
}); })
it('should fail if the alias is not found', async () => { it('should fail if the alias is not found', async () => {
await expect(requests.execByAlias('notAnAlias')).rejects.toThrow(); await expect(requests.execByAlias('notAnAlias')).rejects.toThrow()
}); })
it(`should fail if a given request doesn't have an alias`, () => { it(`should fail if a given request doesn't have an alias`, () => {
let config = new Config({ let config = new Config({
@ -71,8 +71,8 @@ describe('RequestList', () => {
hello: 1 hello: 1
} }
} }
}); })
expect(() => new RequestList(config, config)).toThrow(); expect(() => new RequestList(config, config)).toThrow()
}); })
}); })

View File

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

View File

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

View File

@ -1,8 +1,8 @@
const vm = require('vm'); const vm = require('vm')
const requireg = require('requireg'); const requireg = require('requireg')
const deepmerge = require('deepmerge'); const deepmerge = require('deepmerge')
const { toKebabCase, dynamicValueRegex, replaceInObject } = require('./shared'); const { toKebabCase, dynamicValueRegex, replaceInObject } = require('./shared')
const isPlainObject = require('is-plain-object'); const { isPlainObject } = require('is-plain-object')
class Plugins { class Plugins {
constructor(plugins = [], autoload = ['std']) { constructor(plugins = [], autoload = ['std']) {
@ -10,91 +10,110 @@ class Plugins {
preRequestModifiers: [], preRequestModifiers: [],
postRequestModifiers: [], postRequestModifiers: [],
dynamicValues: [] dynamicValues: []
};
this.context = {};
plugins.forEach(plugin => this.loadPlugin(plugin));
autoload.forEach(plugin => {
try {
requireg.resolve(plugin);
this.loadPlugin(plugin);
} catch (e) {}
});
}
loadPlugin(plugin) {
let name = plugin;
let settings = {};
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];
} }
plugin = requireg(`beau-${toKebabCase(name)}`); this.context = {}
new plugin(this, settings);
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) { executeModifier(modifier, obj, orig) {
let result = deepmerge({}, obj, { isMergeableObject: isPlainObject }); let result = deepmerge({}, obj, { isMergeableObject: isPlainObject })
this.registry[modifier].forEach( this.registry[modifier].forEach(
modifier => (result = modifier(result, orig)) (modifier) => (result = modifier(result, orig))
); )
return result; return result
} }
replaceDynamicValues(obj) { replaceDynamicValues(obj) {
vm.createContext(this.context); vm.createContext(this.context)
return replaceInObject(obj, val => { return replaceInObject(obj, (val) => {
let valIsEmpty = val.trim().length === 0; let valIsEmpty = val.trim().length === 0
if (valIsEmpty) { if (valIsEmpty) {
return val; return val
} }
try { try {
let onlyHasDynamic = let onlyHasDynamic =
val.replace(dynamicValueRegex, '').trim() === ''; val.replace(dynamicValueRegex, '').trim() === ''
if (onlyHasDynamic) { if (onlyHasDynamic) {
let call; let call
val.replace(dynamicValueRegex, (match, c) => { val.replace(dynamicValueRegex, (match, c) => {
call = c; call = c
}); })
return vm.runInContext(call, this.context); return vm.runInContext(call, this.context)
} }
return val.replace(dynamicValueRegex, (match, call) => { return val.replace(dynamicValueRegex, (match, call) => {
return vm.runInContext(call, this.context); return vm.runInContext(call, this.context)
}); })
} catch (e) { } catch (e) {
throw new Error(`DynamicValue: ` + e); throw new Error(`DynamicValue: ` + e)
} }
}); })
} }
addPreRequestModifier(modifier) { addPreRequestModifier(modifier) {
this.registry.preRequestModifiers.push(modifier); this.registry.preRequestModifiers.push(modifier)
} }
addPostRequestModifier(modifier) { addPostRequestModifier(modifier) {
this.registry.postRequestModifiers.push(modifier); this.registry.postRequestModifiers.push(modifier)
} }
defineDynamicValue(name, fn) { defineDynamicValue(name, fn) {
this.registry.dynamicValues.push({ name, fn }); this.registry.dynamicValues.push({ name, fn })
this.context[name] = fn; this.context[name] = fn
} }
} }
module.exports = Plugins; module.exports = Plugins

View File

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

View File

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

View File

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

View File

@ -1,79 +0,0 @@
const Joi = require('joi');
const { requestRegex } = require('./shared.js');
const pluginSchema = [
Joi.string(),
Joi.object()
.keys(null)
.max(1)
];
const requestSchema = [
Joi.object()
.keys({
HEADERS: Joi.object().keys(null),
PAYLOAD: [Joi.object().keys(null), Joi.string()],
PARAMS: Joi.object().keys(null),
FORM: Joi.object().keys(null),
ALIAS: Joi.string().required(),
FORMDATA: Joi.object().keys(null)
})
.without('FORM', ['PAYLOAD', 'FORMDATA'])
.without('PAYLOAD', ['FORM', 'FORMDATA'])
.without('FORMDATA', ['FORM', 'PAYLOAD'])
.rename(/headers/i, 'HEADERS', { override: true })
.rename(/payload/i, 'PAYLOAD', { override: true })
.rename(/params/i, 'PARAMS', { override: true })
.rename(/form/i, 'FORM', { override: true })
.rename(/alias/i, 'ALIAS', { override: true }),
Joi.string()
];
const hostSchema = Joi.object()
.keys({
HOST: Joi.string().required(),
ENDPOINT: Joi.string(),
DEFAULTS: Joi.object().keys(null)
})
.pattern(requestRegex, requestSchema)
.rename(/host/i, 'HOST', { override: true })
.rename(/defaults/i, 'DEFAULTS', { override: true })
.rename(/endpoint/i, 'ENDPOINT', { override: true });
const schema = Joi.object()
.keys({
VERSION: Joi.number().integer(),
ENDPOINT: Joi.string().uri(),
PLUGINS: Joi.array().items(pluginSchema),
DEFAULTS: Joi.object(),
ENVIRONMENT: Joi.object(),
HOSTS: Joi.array().items(hostSchema),
COOKIEJAR: Joi.boolean()
})
.pattern(requestRegex, requestSchema)
.rename(/version/i, 'VERSION', { override: true })
.rename(/endpoint/i, 'ENDPOINT', { override: true })
.rename(/hosts/i, 'HOSTS', { override: true })
.rename(/plugins/i, 'PLUGINS', { override: true })
.rename(/defaults/i, 'DEFAULTS', { override: true })
.rename(/environment/i, 'ENVIRONMENT', { override: true })
.rename(/cookiejar/i, 'COOKIEJAR', { override: true });
const validate = async function(config) {
try {
let results = await Joi.validate(config, schema, {
allowUnknown: true
});
return { valid: true };
} catch ({ name, details }) {
return {
valid: false,
message: `${name}: \n ${details
.map(d => d.message + ' @ ' + d.path)
.join(' \n ')}`
};
}
};
module.exports = { schema, validate };

View File

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