├── .eslintrc
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .npmrc
├── .prettierrc
├── .release-it.json
├── README.md
├── bin
└── dyson.js
├── lib
├── defaults.js
├── delay.js
├── dyson.js
├── favicon.ico
├── loader.js
├── multiRequest.js
├── parameter.js
├── proxy.js
├── raw-body.js
├── response.js
└── util.js
├── package-lock.json
├── package.json
└── test
├── _helpers.js
├── config.js
├── defaults.js
├── dummy
├── bee
│ └── bap
│ │ └── fop
│ │ └── paf.js
├── get
│ ├── dummy.js
│ └── sub
│ │ ├── dummy.js
│ │ └── subsub
│ │ └── dummy.js
└── proxy
│ └── index.js
├── dyson.js
├── fixtures
├── cert.pem
└── key.pem
├── https.js
├── loader.js
├── proxy.js
└── response.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 9,
8 | "sourceType": "module"
9 | },
10 | "extends": ["eslint:recommended", "prettier"],
11 | "plugins": ["prettier"],
12 | "rules": {
13 | "prettier/prettier": ["error", {
14 | "singleQuote": true,
15 | "printWidth": 120,
16 | "trailingComma": "none",
17 | "arrowParens": "avoid"
18 | }]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | strategy:
8 | matrix:
9 | node:
10 | - 10
11 | - 14
12 |
13 | runs-on: ubuntu-latest
14 | name: Node v${{ matrix.node }}
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - run: npm ci
19 | - run: npm test
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "arrowParens": "avoid"
6 | }
7 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "release": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dyson
2 |
3 | Node server for dynamic, fake JSON.
4 |
5 | ## Introduction
6 |
7 | Dyson allows you to define JSON endpoints based on a simple `path` + `template` object:
8 |
9 | ```javascript
10 | # my-stubs/users.js
11 | module.exports = {
12 | path: '/users/:userId',
13 | template: {
14 | id: params => Number(params.userId),
15 | name: () => faker.name.findName(),
16 | email: () => faker.internet.email(),
17 | status: (params, query) => query.status,
18 | lorem: true
19 | }
20 | };
21 | ```
22 |
23 | ```bash
24 | $ dyson ./my-stubs
25 | $ curl http://localhost:3000/users/1?status=active
26 | ```
27 |
28 | ```json
29 | {
30 | "id": 1,
31 | "name": "Josie Greenfelder",
32 | "email": "Raoul_Aufderhar@yahoo.com",
33 | "status": "active",
34 | "lorem": true
35 | }
36 | ```
37 |
38 | When developing client-side applications, often either static JSON files, or an actual server, backend, datastore, or API, is used. Sometimes static files are too static, and sometimes an actual server is not available, not accessible, or too tedious to set up.
39 |
40 | This is where dyson comes in. Get a full fake server for your application up and running in minutes.
41 |
42 | - [Installation notes](#installation)
43 | - [Demo](https://dyson-demo.herokuapp.com)
44 |
45 | [](https://travis-ci.org/webpro/dyson)
46 | [](https://www.npmjs.com/package/dyson)
47 | [](https://david-dm.org/webpro/dyson)
48 | 
49 |
50 | ## Overview
51 |
52 | - Dynamic responses, based on
53 | - Request path
54 | - GET/POST parameters
55 | - Query parameters
56 | - Cookies
57 | - HTTP Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
58 | - Dynamic HTTP status codes
59 | - CORS
60 | - Proxy (e.g. fallback to actual services)
61 | - Delayed responses
62 | - Required parameter validation
63 | - Includes random data generators
64 | - Includes dummy image generator
65 | - Use any external or local image service (included)
66 | - Supports base64 encoded image strings
67 |
68 | ## Endpoint Configuration
69 |
70 | Configure endpoints using simple objects:
71 |
72 | ```javascript
73 | module.exports = {
74 | path: '/user/:id',
75 | method: 'GET',
76 | template: {
77 | id: (params, query, body) => params.id,
78 | name: g.name,
79 | address: {
80 | zip: g.zipUS,
81 | city: g.city
82 | }
83 | }
84 | };
85 | ```
86 |
87 | The `path` string is the usual argument provided to [Express](http://expressjs.com/api.html#app.VERB), as in `app.get(path, callback);`.
88 |
89 | The `template` object may contain properties of the following types:
90 |
91 | - A `Function` will be invoked with arguments `(params, query, body, cookies, headers)`.
92 | - Primitives of type `String`, `Boolean`, `Number`, `Array` are returned as-is
93 | - An `Object` will be recursively iterated.
94 | - A `Promise` will be replaced with its resolved value.
95 |
96 | Note: the `template` itself can also be a _function_ returning the actual data. The template function itself is also invoked with arguments `(params, query, body, cookies, headers)`.
97 |
98 | ## Defaults
99 |
100 | The default values for the configuration objects:
101 |
102 | ```javascript
103 | module.exports = {
104 | cache: false,
105 | delay: false,
106 | proxy: false,
107 | size: () => _.random(2, 10),
108 | collection: false,
109 | callback: response.generate,
110 | render: response.render
111 | };
112 | ```
113 |
114 | - `cache: true` means that multiple requests to the same path will result in the same response
115 | - `delay: n` will delay the response with `n` milliseconds (or between `[n, m]` milliseconds)
116 | - `proxy: false` means that requests to this file can be skipped and sent to the configured proxy
117 | - `size: fn` is the number of objects in the collection
118 | - `collection: true` will return a collection
119 | - `callback: fn`
120 | - the provided default function is doing the hard work (can be overridden)
121 | - used as middleware in Express
122 | - must set `res.body` and call `next()` to render response
123 | - `render: fn`
124 | - the default function to render the response (basically `res.send(200, res.body);`)
125 | - used as middleware in Express
126 |
127 | ## Fake data generators
128 |
129 | You can use _anything_ to generate data. Here are some suggestions:
130 |
131 | - [Faker.js](https://github.com/marak/Faker.js/)
132 | - [Chance.js](http://chancejs.com/)
133 | - [dyson-generators](http://github.com/webpro/dyson-generators)
134 |
135 | Just install the generator(s) in your project to use them in your templates:
136 |
137 | ```bash
138 | npm install dyson-generators --save-dev
139 | ```
140 |
141 | ## Containers
142 |
143 | Containers can help if you need to send along some meta data, or wrap the response data in a specific way. Just use the `container` object, and return the `data` where you want it. Functions in the `container` object are invoked with arguments `(params, query, data)`:
144 |
145 | ```javascript
146 | module.exports = {
147 | path: '/users',
148 | template: user.template,
149 | container: {
150 | meta: (params, query, data) => ({
151 | userCount: data.length
152 | }),
153 | data: {
154 | all: [],
155 | the: {
156 | way: {
157 | here: (params, query, data) => data
158 | }
159 | }
160 | }
161 | }
162 | };
163 | ```
164 |
165 | And an example response:
166 |
167 | ```json
168 | {
169 | "meta": {
170 | "userCount": 2
171 | },
172 | "data": {
173 | "all": [],
174 | "the": {
175 | "way": {
176 | "here": [
177 | {
178 | "id": 412,
179 | "name": "John"
180 | },
181 | {
182 | "id": 218,
183 | "name": "Olivia"
184 | }
185 | ]
186 | }
187 | }
188 | }
189 | }
190 | ```
191 |
192 | ## Combined requests
193 |
194 | Basic support for "combined" requests is available, by means of a comma separated path fragment.
195 |
196 | For example, a request to `/user/5,13` will result in an array of the responses from `/user/5` and `/user/13`.
197 |
198 | The `,` delimiter can be [configured](#project-configuration) (or disabled).
199 |
200 | ## Status codes
201 |
202 | By default, all responses are sent with a status code `200` (and the `Content-Type: application/json` header).
203 |
204 | This can be overridden with your own `status` middleware, e.g.:
205 |
206 | ```javascript
207 | module.exports = {
208 | path: '/feature/:foo?',
209 | status: (req, res, next) => {
210 | if (req.params.foo === '999') {
211 | res.status(404);
212 | }
213 | next();
214 | }
215 | };
216 | ```
217 |
218 | Would result in a `404` when requesting `/feature/999`.
219 |
220 | ## Images
221 |
222 | In addition to configured endpoints, dyson registers a [dummy image service](http://github.com/webpro/dyson-image) at `/image`. E.g. requesting `/image/300x200` serves an image with given dimensions.
223 |
224 | This service is a proxy to [Dynamic Dummy Image Generator](http://dummyimage.com/) by [Russell Heimlich](http://twitter.com/kingkool68).
225 |
226 | ## JSONP
227 |
228 | Override the `render` method of the Express middleware in the endpoint definition. In the example below, depending on the existence of the `callback` parameter, either raw JSON response is returned or it is wrapped with the provided callback:
229 |
230 | ```javascript
231 | module.exports = {
232 | render: (req, res) => {
233 | const callback = req.query.callback;
234 | if (callback) {
235 | res.append('Content-Type', 'application/javascript');
236 | res.send(`${callback}(${JSON.stringify(res.body)});`);
237 | } else {
238 | res.send(res.body);
239 | }
240 | }
241 | };
242 | ```
243 |
244 | ## File Upload
245 |
246 | Ex: return file name
247 | formDataName = 'file'
248 |
249 | ```javascript
250 | module.exports = {
251 | render: (req, res) => {
252 | if (callback) {
253 | res.send({ fileName: req.files.file.name });
254 | } else {
255 | res.send(res.body);
256 | }
257 | }
258 | };
259 | ```
260 |
261 | ## HTTPS
262 |
263 | If you want to run dyson over SSL you have to provide a (authority-signed or self-signed) certificate into the `options.https` the same way it's required for NodeJS built-in `https` module. Example:
264 |
265 | ```javascript
266 | const fs = require('fs');
267 |
268 | const app = dyson.createServer({
269 | configDir: `${__dirname}/dummy`,
270 | port: 3001,
271 | https: {
272 | key: fs.readFileSync(`${__dirname}'/certs/sample.key`),
273 | crt: fs.readFileSync(`${__dirname}/certs/sample.crt`)
274 | }
275 | });
276 | ```
277 |
278 | **Note**: if running SSL on port 443, it will require `sudo` privileges.
279 |
280 | ## GraphQL
281 |
282 | If you want dyson to support GraphQL endpoints, you can build your own logic with the `render` override, or use [`dyson-graphql`](https://github.com/WealthWizardsEngineering/dyson-graphql). Example:
283 |
284 | ```bash
285 | npm install dyson-graphql --save-dev
286 | ```
287 |
288 | ```javascript
289 | const dysonGraphQl = require('dyson-graphql');
290 |
291 | const schema = `
292 | type User {
293 | id: Int!
294 | name: String!
295 | }
296 |
297 | type Query {
298 | currentUser: User!
299 | }
300 |
301 | type Mutation {
302 | createUser(name: String!): User!
303 | updateUser(id: Int!, name: String!): User!
304 | }
305 | `;
306 |
307 | module.exports = {
308 | path: '/graphql',
309 | method: 'POST',
310 | render: dysonGraphQl(schema)
311 | .query('currentUser', { id: 987, name: 'Jane Smart' })
312 | .mutation('createUser', ({ name }) => ({ id: 456, name }))
313 | .mutation('updateUser', ({ id, name }) => {
314 | if (id < 1000) {
315 | return { id, name };
316 | }
317 |
318 | throw new Error("Can't update user");
319 | })
320 | .build()
321 | };
322 | ```
323 |
324 | ## Custom middleware
325 |
326 | If you need some custom middleware before or after the endpoints are registered, dyson can be initialized programmatically.
327 | Then you can use the Express server instance (`appBefore` or `appAfter` in the example below) to install middleware before or after the dyson services are registered. An example:
328 |
329 | ```javascript
330 | const dyson = require('dyson');
331 | const path = require('path');
332 |
333 | const options = {
334 | configDir: path.join(__dirname, 'services'),
335 | port: 8765
336 | };
337 |
338 | const configs = dyson.getConfigurations(options);
339 | const appBefore = dyson.createServer(options);
340 | const appAfter = dyson.registerServices(appBefore, options, configs);
341 |
342 | console.log(`Dyson listening at port ${options.port}`);
343 | ```
344 |
345 | Dyson configuration can also be installed into any Express server:
346 |
347 | ```javascript
348 | const express = require('express');
349 | const dyson = require('./lib/dyson');
350 | const path = require('path');
351 |
352 | const options = {
353 | configDir: path.join(__dirname, 'services')
354 | };
355 |
356 | const myApp = express();
357 | const configs = dyson.getConfigurations(options);
358 |
359 | dyson.registerServices(myApp, options, configs);
360 |
361 | myApp.listen(8765);
362 | ```
363 |
364 | ## Installation
365 |
366 | The recommended way to install dyson is to install it locally and put it in your `package.json`:
367 |
368 | ```bash
369 | npm install dyson --save-dev
370 | ```
371 |
372 | Then you can use it from `scripts` in `package.json` using e.g. `npm run mocks`:
373 |
374 | ```json
375 | {
376 | "name": "my-package",
377 | "version": "1.0.0",
378 | "scripts": {
379 | "mocks": "dyson mocks/"
380 | }
381 | }
382 | ```
383 |
384 | You can also install dyson globally to start it from anywhere:
385 |
386 | ```bash
387 | npm install -g dyson
388 | ```
389 |
390 | ### Project
391 |
392 | You can put your configuration files anywhere. The HTTP method is based on:
393 |
394 | - The `method` property in the configuration itself.
395 | - The folder, or an ancestor folder, containing the configuration is an HTTP method. For example `mocks/post/sub/endpoint.js` will be an endpoint listening to `POST` requests.
396 | - Defaults to `GET`.
397 |
398 | ```bash
399 | dyson [dir]
400 | ```
401 |
402 | This starts the services configured in `[dir]` at [localhost:3000](http://localhost:3000).
403 |
404 | You can also provide an alternative port number by just adding it as a second argument (e.g. `dyson path/ 8181`).
405 |
406 | ### Demo
407 |
408 | - For a demo project, see [webpro/dyson-demo](https://github.com/webpro/dyson-demo).
409 | - This demo was also installed with [Heroku](https://www.heroku.com) to [dyson-demo.herokuapp.com](https://dyson-demo.herokuapp.com).
410 |
411 | ## Project Configuration
412 |
413 | Optionally, you can put a `dyson.json` file next to the configuration folders (inside `[dir]`). It enables to configure some behavior of dyson:
414 |
415 | ```json
416 | {
417 | "multiRequest": ",",
418 | "proxy": true,
419 | "proxyHost": "http://dyson.jit.su",
420 | "proxyPort": 8080,
421 | "proxyDelay": [200, 800]
422 | }
423 | ```
424 |
425 | - Setting `multiRequest` to `false` disables the [combined requests](#combined-requests) feature.
426 | - Setting `bodyParserJsonLimit` or `bodyParserUrlencodedLimit` to `1mb` increases the limit to 1mb from the bodyParser's default of 100kb.
427 | - By default, the `proxy` is set to `false`
428 |
429 | ## Watch/auto-restart
430 |
431 | If you want to automatically restart dyson when you change your configuration objects, you can add [nodemon](https://nodemon.io) as a `devDependency`. Say your configuration files are in the `./api` folder, you can put this in your `package.json`:
432 |
433 | ```
434 | "scripts": {
435 | "mocks": "dyson mocks/",
436 | "watch": "nodemon --watch mocks --exec dyson mocks"
437 | }
438 | ```
439 |
440 | ## Development & run tests
441 |
442 | ```bash
443 | git clone git@github.com:webpro/dyson.git
444 | cd dyson
445 | npm install
446 | npm test
447 | ```
448 |
449 | ## Articles about dyson
450 |
451 | - [How do I create a Fake Server with Dyson? | Apiumhub](https://apiumhub.com/tech-blog-barcelona/create-fake-server-dyson/)
452 | - [Stubbing Network Calls (Api) Using Dyson for Emberjs Apps](http://nepalonrails.herokuapp.com/blog/2014/03/stubbing-network-calls-api-using-dyson-for-emberjs-apps/)
453 | - [Our Ember.js Toolchain](https://nebulab.com/blog/our-ember-js-toolchain)
454 | - [Dyson, construye un servidor de pruebas que devuelva fake JSON para simular una API](https://www.genbeta.com/desarrollo/dyson-construye-un-servidor-de-pruebas-que-devuelva-fake-json-para-simular-una-api)
455 | - [Mockear la capa back con Dyson](https://www.adictosaltrabajo.com/2014/08/27/dyson-fake-json/)
456 | - [Serve JSONP in Dyson](https://grysz.com/2015/12/01/serve-jsonp-in-dyson/)
457 | - Videos
458 | _ [Dyson - HTTP Service mocking](https://www.youtube.com/watch?v=aoSk5Bak-KM)
459 | _ [How to implement HTTP Mock Services into Webpack - Dyson](https://www.youtube.com/watch?v=tfCQOcz9oi4)
460 |
461 | ## License
462 |
463 | [MIT](http://webpro.mit-license.org)
464 |
--------------------------------------------------------------------------------
/bin/dyson.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const _ = require('lodash');
6 | const pkg = require('../package.json');
7 | const { bootstrap } = require('../lib/dyson');
8 |
9 | const [dir, port] = process.argv.slice(2);
10 |
11 | const showHelpAndExit = () => {
12 | console.info(`dyson v${pkg.version}`);
13 | console.info('Usage: dyson
[port]');
14 | process.exit(0);
15 | };
16 |
17 | if (dir) {
18 | let localOpts;
19 |
20 | const configPath = path.join(process.cwd(), dir);
21 | const dirStat = fs.statSync(configPath);
22 |
23 | if (!dirStat || !dirStat.isDirectory()) {
24 | showHelpAndExit();
25 | }
26 |
27 | try {
28 | localOpts = require(path.join(configPath, 'dyson.json'));
29 | } catch (err) {
30 | localOpts = {};
31 | }
32 |
33 | const opts = _.defaults(localOpts, {
34 | port: port || 3000,
35 | configDir: dir,
36 | proxy: false,
37 | multiRequest: ',',
38 | quiet: false
39 | });
40 |
41 | bootstrap(opts);
42 | } else {
43 | showHelpAndExit();
44 | }
45 |
--------------------------------------------------------------------------------
/lib/defaults.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const response = require('./response');
3 |
4 | const getDefaults = method => ({
5 | cache: method === 'get' ? true : false,
6 | delay: false,
7 | proxy: false,
8 | size: () => _.random(2, 10),
9 | collection: false,
10 | status: (req, res, next) => next(),
11 | callback: response.generate,
12 | render: response.render
13 | });
14 |
15 | const assign = configs =>
16 | _.compact(
17 | _.castArray(configs).map(config => {
18 | if (!config || !config.path) {
19 | return;
20 | }
21 |
22 | const method = (config.method || 'get').toLowerCase();
23 | config.method = method;
24 | config = _.defaults(config, getDefaults(method));
25 |
26 | return _.bindAll(config);
27 | })
28 | );
29 |
30 | module.exports = assign;
31 |
--------------------------------------------------------------------------------
/lib/delay.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 |
3 | module.exports = delay => (req, res, next) => {
4 | if (typeof delay === 'number') {
5 | _.delay(next, delay);
6 | } else if (_.isArray(delay)) {
7 | _.delay(next, _.random.apply(null, delay));
8 | } else {
9 | next();
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/lib/dyson.js:
--------------------------------------------------------------------------------
1 | const https = require('https');
2 | const express = require('express');
3 | const favicon = require('serve-favicon');
4 | const bodyParser = require('body-parser');
5 | const cookieParser = require('cookie-parser');
6 | const cors = require('cors');
7 | const { logger } = require('./util');
8 | const assign = require('./defaults');
9 | const load = require('./loader');
10 | const delay = require('./delay');
11 | const proxy = require('./proxy');
12 | const requireParameter = require('./parameter');
13 | const rawBody = require('./raw-body');
14 | const fileUpload = require('express-fileupload');
15 |
16 | /*
17 | * There are roughly 3 steps to initialize dyson:
18 | *
19 | * 1. Load user configurations
20 | * 2. Create Express server
21 | * 3. Register configured services with Express
22 | */
23 |
24 | const bootstrap = options => {
25 | const configs = load(options.configDir);
26 |
27 | const app = createServer({
28 | https: options.https,
29 | port: options.port
30 | });
31 |
32 | return registerServices(app, options, configs);
33 | };
34 |
35 | const createServer = (options = {}) => {
36 | const app = express();
37 |
38 | if (options.https) {
39 | https.createServer(options.https, app).listen(options.port);
40 | } else {
41 | app.listen(options.port);
42 | }
43 |
44 | return app;
45 | };
46 |
47 | const setConfig = config => (req, res, next) => {
48 | res.locals.config = config;
49 | next();
50 | };
51 |
52 | const installMiddleware = (app, options) => {
53 | const bodyParserOptions = {};
54 | if (options.bodyParserJsonLimit) {
55 | bodyParserOptions.limit = options.bodyParserJsonLimit;
56 | }
57 | if (options.bodyParserJsonStrict !== undefined) {
58 | bodyParserOptions.strict = options.bodyParserJsonStrict;
59 | }
60 | const bodyParserUrlOptions = { extended: true };
61 | bodyParserUrlOptions.limit = options.bodyParserUrlencodedLimit ? options.bodyParserUrlencodedLimit : null;
62 |
63 | app.use(cors({ origin: true, credentials: true }));
64 | app.use(rawBody());
65 | app.use(cookieParser());
66 | app.use(favicon(`${__dirname}/favicon.ico`));
67 | app.use(bodyParser.json(bodyParserOptions));
68 | app.use(bodyParser.urlencoded(bodyParserUrlOptions));
69 | app.use(fileUpload());
70 |
71 | return app;
72 | };
73 |
74 | // Register middleware to Express as service for each config (as in: `app.get(config.path, config.callback);`)
75 | const registerServices = (app, options, configs) => {
76 | app.set('dyson_logger', logger(options));
77 | app.set('dyson_options', options);
78 |
79 | const { log, err } = app.get('dyson_logger');
80 |
81 | installMiddleware(app, options);
82 |
83 | configs = assign(configs);
84 |
85 | configs.forEach(config => {
86 | const method = config.method;
87 | const isProxied = options.proxy === true && config.proxy !== false;
88 | if (isProxied) {
89 | log('Proxying', method.toUpperCase(), 'service at', config.path);
90 | } else {
91 | const middlewares = [
92 | setConfig(config),
93 | requireParameter,
94 | config.status,
95 | config.callback,
96 | delay(config.delay),
97 | config.render
98 | ];
99 |
100 | log('Registering', method.toUpperCase(), 'service at', config.path);
101 | app[method](config.path, ...middlewares);
102 |
103 | if (method !== 'options') {
104 | app.options(config.path, cors({ origin: true, credentials: true }));
105 | }
106 | }
107 | });
108 |
109 | if (options.proxy) {
110 | app.all('*', delay(options.proxyDelay), proxy);
111 | } else {
112 | app.all('*', (req, res) => {
113 | err(`404 NOT FOUND: ${req.url}`);
114 | res.writeHead(404);
115 | res.end();
116 | });
117 | }
118 |
119 | return app;
120 | };
121 |
122 | module.exports = {
123 | bootstrap,
124 | createServer,
125 | registerServices,
126 | // TODO: deprecated:
127 | getConfigurations: options => load(options.configDir)
128 | };
129 |
--------------------------------------------------------------------------------
/lib/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webpro/dyson/a05906fd1381d22dfab1a8c4789e3fad44b18f5b/lib/favicon.ico
--------------------------------------------------------------------------------
/lib/loader.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 | const requireDir = require('require-directory');
3 | const path = require('path');
4 |
5 | const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
6 |
7 | const load = configDir => {
8 | const rawConfigs = requireDir(module, path.resolve(configDir));
9 | return _.flattenDeep(findRecursive(rawConfigs));
10 | };
11 |
12 | const findRecursive = obj => {
13 | const configs = [];
14 | for (const key in obj) {
15 | const config = obj[key];
16 | if (_.isObject(config)) {
17 | const _config = { ...config };
18 | const _key = key.toLowerCase();
19 | const method = (_config.method || '').toLowerCase();
20 | _config.method = method || (~methods.indexOf(_key) ? _key : obj.method || undefined);
21 | if ('path' in _config) {
22 | configs.push(_config);
23 | } else {
24 | configs.push(findRecursive(_config));
25 | }
26 | }
27 | }
28 | return configs;
29 | };
30 |
31 | module.exports = load;
32 |
--------------------------------------------------------------------------------
/lib/multiRequest.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 |
3 | const isMultiRequest = (path, options) => {
4 | const delimiter = options.multiRequest;
5 | return delimiter ? path.split('/').find(fragment => fragment.includes(delimiter)) : false;
6 | };
7 |
8 | const doMultiRequest = (req, path) => {
9 | const options = req.app.get('dyson_options');
10 | const { err } = req.app.get('dyson_logger');
11 | const [hostname, port] = req.headers.host.split(':');
12 | const delimiter = options.multiRequest;
13 | const range = isMultiRequest(path, options);
14 |
15 | return range.split(delimiter).map((id, index, list) => {
16 | const url = path.replace(list, id);
17 | let data = '';
18 |
19 | return new Promise((resolve, reject) => {
20 | http
21 | .get({ hostname, port, path: url }, res => {
22 | res.on('data', chunk => {
23 | data += chunk;
24 | });
25 | res.on('end', () => {
26 | resolve(JSON.parse(data));
27 | });
28 | })
29 | .on('error', error => {
30 | err(error.message);
31 | reject(error);
32 | });
33 | });
34 | });
35 | };
36 |
37 | module.exports = {
38 | isMultiRequest,
39 | doMultiRequest
40 | };
41 |
--------------------------------------------------------------------------------
/lib/parameter.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash');
2 |
3 | module.exports = (req, res, next) => {
4 | const { requireParameters } = res.locals.config;
5 | const { body, query } = req;
6 | if (!_.isEmpty(requireParameters)) {
7 | const missingParameters = requireParameters.filter(
8 | parameter => _.isEmpty(body[parameter]) && _.isEmpty(query[parameter])
9 | );
10 |
11 | if (!_.isEmpty(missingParameters)) {
12 | const error = `Required parameters (${missingParameters.join(', ')}) not found.`;
13 | res.status(400).send({ error });
14 | return;
15 | }
16 | }
17 | next();
18 | };
19 |
--------------------------------------------------------------------------------
/lib/proxy.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const Stream = require('stream');
3 | const _ = require('lodash');
4 |
5 | module.exports = (req, res) => {
6 | const { log, err } = req.app.get('dyson_logger');
7 | const options = req.app.get('dyson_options');
8 | const { proxyHost, proxyPort } = options;
9 | const proxyURI = `${proxyHost}${proxyPort ? `:${proxyPort}` : ''}${req.url}`;
10 |
11 | let readStream;
12 |
13 | log(`Proxying ${req.url} to ${proxyURI}`);
14 |
15 | if (req._body) {
16 | readStream = new Stream.Readable();
17 | readStream._read = function () {
18 | this.push(req.rawBody);
19 | this.push(null);
20 | };
21 | } else {
22 | readStream = req;
23 | }
24 |
25 | readStream
26 | .pipe(
27 | request(
28 | {
29 | method: req.method,
30 | url: proxyURI,
31 | headers: _.omit(req.headers, ['host'])
32 | },
33 | error => {
34 | if (error) {
35 | err(`500 INTERNAL SERVER ERROR: ${proxyURI}`);
36 | err(error);
37 | res.writeHead(500);
38 | res.end();
39 | }
40 | }
41 | )
42 | )
43 | .pipe(res);
44 | };
45 |
--------------------------------------------------------------------------------
/lib/raw-body.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ property = 'rawBody' } = {}) => (req, res, next) => {
2 | let data = '';
3 | req.on('data', chunk => {
4 | data += chunk;
5 | });
6 | req.on('end', () => {
7 | req[property] = data;
8 | });
9 | next();
10 | };
11 |
--------------------------------------------------------------------------------
/lib/response.js:
--------------------------------------------------------------------------------
1 | const multiRequest = require('./multiRequest');
2 | const _ = require('lodash');
3 |
4 | const cache = {};
5 |
6 | const result = (prop, args) => (_.isFunction(prop) ? prop(...args) : prop);
7 |
8 | const generate = (req, res, next) => {
9 | const { config } = res.locals;
10 | const options = res.app.get('dyson_options');
11 | const { log } = res.app.get('dyson_logger');
12 | const path = req.url;
13 | const cacheKey = req.method + req.url;
14 | const exposeRequest = config.exposeRequest || (options.exposeRequest && config.exposeRequest !== false);
15 |
16 | const templateArgs = exposeRequest ? [req] : [req.params, req.query, req.body, req.cookies, req.headers];
17 | const containerArgs = exposeRequest ? [req] : [req.params, req.query];
18 |
19 | if (config.cache && cache[cacheKey]) {
20 | log('Resolving response for', req.method, path, '(cached)');
21 | res.body = cache[cacheKey];
22 | return next();
23 | }
24 |
25 | if (multiRequest.isMultiRequest(path, options)) {
26 | Promise.all(multiRequest.doMultiRequest(req, path)).then(data => {
27 | res.body = cache[cacheKey] = data;
28 | log('Resolving response for:', req.method, path, '(multiRequest)');
29 | next();
30 | });
31 | return;
32 | }
33 |
34 | const template = result(config.template, templateArgs);
35 | const isCollection = result(config.collection, templateArgs);
36 | const size = result(config.size, templateArgs);
37 |
38 | const responseAwait = !isCollection
39 | ? assembleResponse(template, templateArgs)
40 | : Promise.all(_.times(size, () => assembleResponse(template, templateArgs)));
41 |
42 | responseAwait
43 | .then(response =>
44 | !config.container
45 | ? response
46 | : assembleResponse(_.result(config, 'container'), [...containerArgs, response], config)
47 | )
48 | .then(data => {
49 | res.body = cache[cacheKey] = data;
50 | log('Resolving response for', req.method, path);
51 | next();
52 | });
53 | };
54 |
55 | const isPromiseLike = obj => _.isObject(obj) && 'then' in obj;
56 |
57 | const assembleResponse = (template = null, params, scope) =>
58 | Promise.resolve().then(() => {
59 | if (!template) return null;
60 | if (typeof template === 'string') return template;
61 | if (isPromiseLike(template)) return template;
62 |
63 | const obj = _.isArray(template) ? [] : {};
64 |
65 | return Promise.all(
66 | _.map(template, (value, key) => {
67 | if (Object.prototype.hasOwnProperty.call(template, key)) {
68 | obj[key] = _.isFunction(value)
69 | ? value.apply(scope || obj, params)
70 | : _.isPlainObject(value)
71 | ? assembleResponse(value, params, obj)
72 | : value;
73 | if (isPromiseLike(obj[key])) {
74 | return obj[key].then(value => {
75 | obj[key] = value;
76 | });
77 | }
78 | }
79 | })
80 | ).then(() => obj);
81 | });
82 |
83 | const render = (req, res) => res.send(res.body);
84 |
85 | module.exports = {
86 | generate,
87 | render,
88 | assembleResponse
89 | };
90 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | const logger = options => {
2 | /* eslint-disable no-console */
3 | const isQuiet = options.quiet !== false;
4 | return {
5 | log: (...args) => !isQuiet && console.log(...args),
6 | err: (...args) => !isQuiet && console.error(...args)
7 | };
8 | };
9 |
10 | module.exports = {
11 | logger
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dyson",
3 | "version": "4.1.0",
4 | "description": "Node server for dynamic, fake JSON.",
5 | "keywords": [
6 | "API",
7 | "JSON",
8 | "REST",
9 | "data",
10 | "dummy",
11 | "dynamic",
12 | "fake",
13 | "generator",
14 | "proxy",
15 | "response",
16 | "server"
17 | ],
18 | "author": {
19 | "email": "lars@webpro.nl",
20 | "name": "Lars Kappert"
21 | },
22 | "main": "./lib/dyson.js",
23 | "bin": {
24 | "dyson": "./bin/dyson.js"
25 | },
26 | "scripts": {
27 | "test": "bron test/*.js",
28 | "lint": "eslint lib test",
29 | "format": "prettier --write \"{bin,lib,test}/**/*.js\"",
30 | "release": "release-it"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "https://github.com/webpro/dyson"
35 | },
36 | "homepage": "https://github.com/webpro/dyson#readme",
37 | "bugs": "https://github.com/webpro/dyson/issues",
38 | "files": [
39 | "bin",
40 | "lib"
41 | ],
42 | "engines": {
43 | "node": ">=10"
44 | },
45 | "dependencies": {
46 | "body-parser": "^1.19.0",
47 | "cookie-parser": "^1.4.5",
48 | "cors": "^2.8.5",
49 | "express": "^4.17.1",
50 | "express-fileupload": "1.2.1",
51 | "lodash": "^4.17.21",
52 | "request": "^2.88.2",
53 | "require-directory": "^2.1.1",
54 | "serve-favicon": "^2.5.0"
55 | },
56 | "devDependencies": {
57 | "bron": "^1.1.1",
58 | "dyson-generators": "^0.2.1",
59 | "dyson-image": "^0.2.1",
60 | "eslint": "7.28.0",
61 | "eslint-config-prettier": "8.3.0",
62 | "eslint-plugin-prettier": "3.4.0",
63 | "prettier": "2.3.1",
64 | "release-it": "14.8.0",
65 | "sinon": "11.1.1",
66 | "supertest": "^6.1.3"
67 | },
68 | "license": "MIT"
69 | }
70 |
--------------------------------------------------------------------------------
/test/_helpers.js:
--------------------------------------------------------------------------------
1 | const dyson = require('../lib/dyson');
2 |
3 | const getService = (config, options = {}) => {
4 | const app = dyson.createServer(options);
5 | return config ? dyson.registerServices(app, options, config) : app;
6 | };
7 |
8 | module.exports = { getService };
9 |
--------------------------------------------------------------------------------
/test/config.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const _ = require('lodash');
4 | const request = require('supertest');
5 | const { getService } = require('./_helpers');
6 |
7 | test('should return cached response', async () => {
8 | let id = 0;
9 | const app = getService({
10 | path: '/cache',
11 | template: {
12 | id: () => id++
13 | }
14 | });
15 |
16 | const res = await request(app).get('/cache');
17 | const cachedRes = await request(app).get('/cache');
18 |
19 | assert.equal(cachedRes.status, 200);
20 | assert.deepEqual(cachedRes.body, res.body);
21 | });
22 |
23 | test('should not cache the response with a different method', async () => {
24 | let id = 0;
25 | const app = getService([
26 | {
27 | path: '/cache',
28 | cache: false,
29 | method: 'GET',
30 | template: {
31 | id: () => id++
32 | }
33 | },
34 | {
35 | path: '/cache',
36 | cache: false,
37 | method: 'POST',
38 | template: {
39 | id: () => id++
40 | }
41 | }
42 | ]);
43 |
44 | const res = await request(app).get('/cache');
45 | const cachedRes = await request(app).post('/cache');
46 |
47 | assert.equal(cachedRes.status, 200);
48 | assert.notDeepEqual(cachedRes.body, res.body);
49 | });
50 |
51 | test('should return uncached response', async () => {
52 | let id = 0;
53 | const app = getService({
54 | path: '/no-cache',
55 | cache: false,
56 | template: {
57 | id: () => id++
58 | }
59 | });
60 |
61 | const res = await request(app).get('/no-cache');
62 | const uncachedRes = await request(app).get('/no-cache');
63 |
64 | assert.equal(uncachedRes.status, 200);
65 | assert.notDeepEqual(uncachedRes.body, res.body);
66 | });
67 |
68 | test('should respond with a collection', async () => {
69 | let id = 0;
70 | const config = {
71 | path: '/collection',
72 | collection: true,
73 | size: 2,
74 | template: {
75 | id: () => ++id
76 | }
77 | };
78 |
79 | const app = getService([
80 | config,
81 | {
82 | ...config,
83 | path: '/collection-as-function',
84 | collection: () => true
85 | },
86 | {
87 | ...config,
88 | path: '/collection-as-function-negative',
89 | collection: () => false
90 | },
91 | {
92 | ...config,
93 | path: '/size-as-function',
94 | size: (params, query) => query.count
95 | }
96 | ]);
97 |
98 | const res1 = await request(app).get('/collection');
99 | const res2 = await request(app).get('/collection-as-function');
100 | const res3 = await request(app).get('/collection-as-function-negative');
101 | const res4 = await request(app).get('/size-as-function?count=3');
102 |
103 | assert.equal(res1.status, 200);
104 | assert.equal(res2.status, 200);
105 | assert.equal(res3.status, 200);
106 | assert.equal(res4.status, 200);
107 |
108 | assert.deepEqual(res1.body, [{ id: 1 }, { id: 2 }]);
109 | assert.deepEqual(res2.body, [{ id: 3 }, { id: 4 }]);
110 | assert.deepEqual(res3.body, { id: 5 });
111 | assert.deepEqual(res4.body, [{ id: 6 }, { id: 7 }, { id: 8 }]);
112 | });
113 |
114 | test('should respond with a collection (combined request)', async () => {
115 | const config = {
116 | path: '/combined/:id',
117 | template: {
118 | id: params => Number(params.id)
119 | }
120 | };
121 | const app = getService(config, {
122 | multiRequest: ','
123 | });
124 |
125 | const res = await request(app).get('/combined/1,2,3');
126 |
127 | assert.equal(res.status, 200);
128 | assert.deepEqual(res.body, [{ id: 1 }, { id: 2 }, { id: 3 }]);
129 | });
130 |
131 | test('should respond with a 204 for an OPTIONS request', async () => {
132 | const app = getService({
133 | path: '/opts',
134 | template: []
135 | });
136 |
137 | const res = await request(app).options('/opts');
138 |
139 | assert.equal(res.status, 204);
140 | assert.equal(res.headers['access-control-allow-methods'], 'GET,HEAD,PUT,PATCH,POST,DELETE');
141 | assert.equal(res.headers['access-control-allow-credentials'], 'true');
142 | // The next actual value is 'undefined', should be req.header('Origin') (probably an issue with supertest)
143 | // assert.equal(res.headers['access-control-allow-origin'], '*');
144 | });
145 |
146 | test('should respond with 400 bad request if required parameter not found', async () => {
147 | const app = getService({
148 | path: '/require-param',
149 | requireParameters: ['name'],
150 | template: []
151 | });
152 |
153 | const res = await request(app).get('/require-param');
154 |
155 | assert.equal(res.status, 400);
156 | assert.deepEqual(res.body, { error: 'Required parameters (name) not found.' });
157 |
158 | const resParam = await request(app).get('/require-param?name=foo');
159 | assert.equal(resParam.status, 200);
160 | assert.deepEqual(resParam.body, []);
161 | });
162 |
163 | test('should delay the response', async () => {
164 | const app = getService({
165 | path: '/delay',
166 | delay: 200,
167 | template: []
168 | });
169 |
170 | const start = _.now();
171 | const res = await request(app).get('/delay');
172 | const delayed = _.now() - start;
173 |
174 | assert.equal(res.status, 200);
175 | assert(delayed >= 200);
176 | });
177 |
178 | test('should support status function', async () => {
179 | const app = getService({
180 | path: '/status-418',
181 | status: (req, res, next) => {
182 | res.status(418);
183 | next();
184 | },
185 | template: ['foo', 'bar']
186 | });
187 |
188 | const res = await request(app).get('/status-418');
189 |
190 | assert.equal(res.status, 418);
191 | assert.deepEqual(res.body, ['foo', 'bar']);
192 | });
193 |
194 | test('should support HEAD requests', async () => {
195 | const app = getService({
196 | path: '/head',
197 | method: 'HEAD'
198 | });
199 |
200 | const res = await request(app).head('/head');
201 |
202 | assert.equal(res.status, 200);
203 | });
204 |
--------------------------------------------------------------------------------
/test/defaults.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const assign = require('../lib/defaults');
4 |
5 | test('assert should apply defaults (and not overwrite existing values)', () => {
6 | const config = {
7 | path: '/test',
8 | template: {}
9 | };
10 |
11 | assign(config, 'get');
12 |
13 | assert.equal(config.path, '/test');
14 | assert.equal(config.cache, true);
15 | assert.equal(config.collection, false);
16 | assert.equal(typeof config.size, 'function');
17 | assert.equal(typeof config.callback, 'function');
18 | assert.deepEqual(config.template, {});
19 | });
20 |
21 | test('assert should bind config methods to the config', () => {
22 | let counter = 0;
23 |
24 | const config = {
25 | path: '/test',
26 | template: {},
27 | callback: function () {
28 | counter++;
29 | return this;
30 | },
31 | render: function () {
32 | counter++;
33 | return this;
34 | }
35 | };
36 |
37 | assign(config, 'get');
38 |
39 | const c = config.callback().render();
40 |
41 | assert.equal(counter, 2);
42 | assert.deepEqual(c, config);
43 | });
44 |
--------------------------------------------------------------------------------
/test/dummy/bee/bap/fop/paf.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | path: '/dummy-four',
3 | method: 'PATCH',
4 | proxy: false,
5 | template: {
6 | dummy: 'OK'
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/dummy/get/dummy.js:
--------------------------------------------------------------------------------
1 | const g = require('dyson-generators');
2 |
3 | module.exports = {
4 | path: '/dummy/:id?',
5 | template: {
6 | id: params => Number(params.id || 1),
7 | name: g.name,
8 | dummy: 'OK'
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/test/dummy/get/sub/dummy.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | path: '/dummy-two',
3 | proxy: false,
4 | template: {
5 | id: params => {
6 | return params.id || 1;
7 | },
8 | name: 'Dummy two',
9 | dummy: 'OK'
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/test/dummy/get/sub/subsub/dummy.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | path: '/dummy-three',
3 | proxy: false,
4 | template: {
5 | id: params => {
6 | return params.id || 1;
7 | },
8 | name: 'Dummy three',
9 | dummy: 'OK'
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/test/dummy/proxy/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | path: '/proxy',
3 | method: 'GET',
4 | template: {
5 | isProxy: true
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/test/dyson.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const sinon = require('sinon');
4 | const dyson = require('../lib/dyson');
5 | const getService = require('./_helpers').getService;
6 |
7 | test('should add GET route to Express', () => {
8 | const app = getService();
9 | const spy = sinon.spy(app, 'get');
10 | const config = {
11 | path: '/',
12 | status: () => {},
13 | callback: () => {},
14 | render: () => {}
15 | };
16 |
17 | dyson.registerServices(app, {}, config);
18 |
19 | assert.equal(spy.lastCall.args[0], '/');
20 | assert(spy.lastCall.args.includes(config.status));
21 | assert(spy.lastCall.args.includes(config.callback));
22 | assert(spy.lastCall.args.includes(config.render));
23 | });
24 |
25 | test('should add POST route to Express', () => {
26 | const app = getService();
27 | const spy = sinon.spy(app, 'post');
28 | const config = {
29 | path: '/',
30 | method: 'POST'
31 | };
32 |
33 | dyson.registerServices(app, {}, config);
34 |
35 | assert.equal(spy.firstCall.args[0], '/');
36 | });
37 |
38 | test('should add OPTIONS route to Express', () => {
39 | const app = getService();
40 | const spy = sinon.spy(app, 'options');
41 | const config = {
42 | path: '/',
43 | method: 'GET'
44 | };
45 |
46 | dyson.registerServices(app, {}, config);
47 |
48 | assert.equal(spy.firstCall.args[0], '/');
49 | assert.equal(typeof spy.firstCall.args[1], 'function');
50 | });
51 |
--------------------------------------------------------------------------------
/test/fixtures/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICpDCCAYwCCQCP3UEJzq8t6DANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDEwls
3 | b2NhbGhvc3QwHhcNMTMwOTExMTQzNDM1WhcNMTMxMDExMTQzNDM1WjAUMRIwEAYD
4 | VQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5
5 | PH7dzxu9F9MOPJHvy0osayMgMdUUOj7A1RSjM4dEGUb+Ex99uSC3i2m+ftQ2ahPY
6 | jhyowt3sOCFSnMNe1bgd6C1DLLShrir/ZarzB5bKItCiG0oPOOxXHOuDHoA6bpqi
7 | ZGJvWeFQBPNzUL4xmF6wCIOt69d0yMMYFqEk8bLZ/Nba6b3uxLr9ZaDU1ZeAsu1g
8 | Z1s2ST71Uvn03YEAc19yKaCnTH1BfWMkV2thdWQZ4/d3Dvovb4yUENL4VaQzloyy
9 | ptVP4frYZTEo2VD1dSpH6gb13s4wdiaznDJNvqbdZM4A/jSgP9gvmMztuhZdn4SR
10 | qwnPoGVIVxtIWijYlGF5AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAHRAO+/f6Yvm
11 | qMWO/Y0atI5gZ9/FGuw8OPMiNdbLa/0kLOaqlAcxn6GD1jMIjyalIQz22dddDKJ3
12 | 7OqVF/uldLypuEIFyoHAqy8IRQYUXsqgIeW2V+6T/fGS0QWoEWUim8D9Cwxs5hnA
13 | kLf4KrIihSG5RzjuJ7RbGMF+3UMOGuXQy8m99eI8O/MNjyhk6EnPgU5ghL3wk4pb
14 | X+sgh3bUN5NHE1Nmh94l9k1XMyVZniDbnM2DhVf3kkkDHQPZwtNVZL5/jsLZfFLC
15 | FqHdPk1xgGFxoQsRgHN78LYp113yxyPfWdKRVmizaZsxm4/FRdt/O+bUVfgg4X6x
16 | dgH8W+6R8rI=
17 | -----END CERTIFICATE-----
18 |
--------------------------------------------------------------------------------
/test/fixtures/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEAuTx+3c8bvRfTDjyR78tKLGsjIDHVFDo+wNUUozOHRBlG/hMf
3 | fbkgt4tpvn7UNmoT2I4cqMLd7DghUpzDXtW4HegtQyy0oa4q/2Wq8weWyiLQohtK
4 | DzjsVxzrgx6AOm6aomRib1nhUATzc1C+MZhesAiDrevXdMjDGBahJPGy2fzW2um9
5 | 7sS6/WWg1NWXgLLtYGdbNkk+9VL59N2BAHNfcimgp0x9QX1jJFdrYXVkGeP3dw76
6 | L2+MlBDS+FWkM5aMsqbVT+H62GUxKNlQ9XUqR+oG9d7OMHYms5wyTb6m3WTOAP40
7 | oD/YL5jM7boWXZ+EkasJz6BlSFcbSFoo2JRheQIDAQABAoIBAHyPp6g0ayy+5pf+
8 | NwyPIXO5H8e4eta9TBGTt+r+7YjnjouwBE8gvFVwlE0bMEzfDDVlavQ5Bc6g+Bd7
9 | fw04njTOOhGf8F+ApT1U+p2ujsGio7U+sJCH8LWrpttnGUcxtR5abq7+O7r5eVQk
10 | CaGEGrg5IYNEwn+vuTFrljUnquNWM17NNJ83Qj8B5JGqvuVaP5iclw2AmfCHe6aG
11 | sXEo739T47jY6YBwSvPbtELWMMgciqJ7ZRw9Cr72/MM51jw5AhCC4ZqZswE4fs5T
12 | T0zEZpoyDE44ylx9Ics2goAfDdCGhS/C5sgNN/AkkecVQuY53lBt/js6zhSCvgkz
13 | wAQxagECgYEA4/5NFP2hMWruO8yt2B5UytCzvfBRXmJcooY927BBHdtYd9lv0sid
14 | 7yZR81KfsUCXKhHGrUFPPdFPEkgNwi6QOExT9Apf+VSGVm6XObEEEyqT2enyVsiT
15 | xgPVrgDKj6IDc3fXHA4wcsVxaVJ4qIt4rWtG3x6oEdZOC1bsvjkCAqECgYEAz/2d
16 | cRGxXCgCzNEjD/QkaW/hEMEnmFnLReOaIKBVkhxzD2FEFhSrem19uZddj3KIPC2E
17 | NGU1ZmcyqCaAOJt7DYiOX9zqhc12Cai26S3D8OlavSc4J04AqfJs697Ok7VXxUSA
18 | jO4BCVi/UNtzAKmK8tVpsWqv/ETTUJXVUdq1x9kCgYEAuigeuh/paNc9lBgobgk+
19 | BKfpyxGY7q7zokRn56P/VyiNELa6hmoGAonQahOxjmIFy3TeOwLTd88ad/vbOA0a
20 | 9szj06RQ/tzUH2iHE7UEdb3TIR/THqcBebIR29SLkEGh/bsBKcgwKNYsJuoO2Neg
21 | fkDUikOWyZGpAbtE7IDRsmECgYBxgIFOls0m8V61zttHdX/5WeiEcCPfbAEV3qLZ
22 | cyW/Wm8f0YCKXDVH1kBp60RPZ70Yue4PebuualqmkHwgaBi6xe6MOc5xvjHQC5Xl
23 | oefvrCisWJ64NEUAeR8fiLNKwAdpy3wrbCZ8p0WgJmGX1u3Qns3S19m53QVEUL/c
24 | r3HL4QKBgFpWeWZKwdaJ+4iPxiMdgUVrPNw435QpBVricANLVCl0Z93tIc5vB2CN
25 | YwzdQOM+o94+oaswEefWksl4YX0x44lKQMFxhAuYRhj/vezrrCy0bDPinu065azG
26 | FZ31YMFD84NUYGgjTh9cpAoMydqtdjzn6Puvh3lGr0YFzgIhoR7t
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/test/https.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const fs = require('fs');
4 | const path = require('path');
5 | const request = require('supertest');
6 | const getService = require('./_helpers').getService;
7 |
8 | const key = fs.readFileSync(path.join(__dirname, 'fixtures', 'key.pem'));
9 | const cert = fs.readFileSync(path.join(__dirname, 'fixtures', 'cert.pem'));
10 |
11 | test('https request should respond with correct body', async () => {
12 | const options = {
13 | port: 8765,
14 | https: {
15 | key: key,
16 | crt: cert
17 | }
18 | };
19 |
20 | const config = {
21 | path: '/secure',
22 | template: {
23 | foo: 'bar'
24 | }
25 | };
26 |
27 | const app = getService(config, options);
28 |
29 | const res = await request(app).get('/secure').ca(cert);
30 |
31 | assert.equal(res.status, 200);
32 | assert.deepEqual(res.body, {
33 | foo: 'bar'
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/loader.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const load = require('../lib/loader');
4 | const _ = require('lodash');
5 |
6 | const configDir = __dirname + '/dummy';
7 |
8 | test('load should return configuration for each method found', () => {
9 | const configs = load(configDir);
10 |
11 | assert.equal(configs.length, 5);
12 | assert.equal(_.filter(configs, { method: 'get' }).length, 4);
13 | assert.equal(_.filter(configs, { method: 'patch' }).length, 1);
14 | });
15 |
--------------------------------------------------------------------------------
/test/proxy.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const request = require('supertest');
4 | const getService = require('./_helpers').getService;
5 |
6 | test('should proxy', async () => {
7 | const config = {
8 | path: '/proxy',
9 | method: 'GET',
10 | template: {
11 | isProxy: true
12 | }
13 | };
14 |
15 | const options = {
16 | port: 3001
17 | };
18 |
19 | const proxyOptions = {
20 | port: 3000,
21 | proxy: true,
22 | proxyHost: 'http://127.0.0.1',
23 | proxyPort: 3001,
24 | proxyDelay: 0
25 | };
26 |
27 | getService(config, options);
28 | const proxy = getService({}, proxyOptions);
29 |
30 | const res = await request(proxy).get('/proxy');
31 |
32 | assert.equal(res.status, 200);
33 | assert.deepEqual(res.body, {
34 | isProxy: true
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/response.js:
--------------------------------------------------------------------------------
1 | const test = require('bron');
2 | const assert = require('assert').strict;
3 | const sinon = require('sinon');
4 | const request = require('supertest');
5 | const { assembleResponse } = require('../lib/response');
6 | const { getService } = require('./_helpers');
7 |
8 | test('should return a promise', async () => {
9 | const awaitResponse = assembleResponse({});
10 | assert.equal(typeof awaitResponse.then, 'function');
11 | assert.deepEqual(await awaitResponse, {});
12 | });
13 |
14 | test('should render data based on template', async () => {
15 | assert.deepEqual(
16 | await assembleResponse({
17 | myFunction: () => {
18 | return 'my function';
19 | },
20 | myString: 'my string',
21 | myBoolean: true,
22 | myNumber: 42,
23 | myArray: [1, 2, 3]
24 | }),
25 | {
26 | myFunction: 'my function',
27 | myString: 'my string',
28 | myBoolean: true,
29 | myNumber: 42,
30 | myArray: [1, 2, 3]
31 | }
32 | );
33 | });
34 |
35 | test('should return an array', async () => {
36 | assert.deepEqual(
37 | await assembleResponse([
38 | () => {
39 | return 'my function';
40 | },
41 | 2,
42 | {}
43 | ]),
44 | ['my function', 2, {}]
45 | );
46 | });
47 |
48 | test('should parse template objects recursively', async () => {
49 | assert.deepEqual(
50 | await assembleResponse({
51 | myObject: {
52 | myNestedObject: {
53 | myDeepFunction: () => {
54 | return 'my other function';
55 | },
56 | myDeepString: 'my other string'
57 | }
58 | }
59 | }),
60 | {
61 | myObject: {
62 | myNestedObject: {
63 | myDeepFunction: 'my other function',
64 | myDeepString: 'my other string'
65 | }
66 | }
67 | }
68 | );
69 | });
70 |
71 | test('should replace a promise with its resolved value', async () => {
72 | assert.deepEqual(
73 | await assembleResponse({
74 | myPromiseFn: () => Promise.resolve('my promise'),
75 | myPromise: Promise.resolve('my promise')
76 | }),
77 | {
78 | myPromiseFn: 'my promise',
79 | myPromise: 'my promise'
80 | }
81 | );
82 | });
83 |
84 | test('should expose request to template', async () => {
85 | const spy = sinon.spy();
86 | const app = getService({
87 | path: '/foo',
88 | exposeRequest: true,
89 | template: spy,
90 | container: {
91 | foo: spy
92 | }
93 | });
94 |
95 | await request(app).get('/foo');
96 |
97 | assert(spy.callCount === 2);
98 | assert.equal(spy.firstCall.args[0], spy.secondCall.args[0]);
99 |
100 | const req = spy.firstCall.args[0];
101 |
102 | assert('params' in req);
103 | assert('query' in req);
104 | assert('body' in req);
105 | assert('cookies' in req);
106 | assert('headers' in req);
107 | });
108 |
--------------------------------------------------------------------------------