├── .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 | [![Build Status](https://img.shields.io/travis/webpro/dyson.svg?style=flat)](https://travis-ci.org/webpro/dyson) 46 | [![npm package](https://img.shields.io/npm/v/dyson.svg?style=flat)](https://www.npmjs.com/package/dyson) 47 | [![dependencies](https://img.shields.io/david/webpro/dyson.svg?style=flat)](https://david-dm.org/webpro/dyson) 48 | ![npm version](https://img.shields.io/node/v/dyson.svg?style=flat) 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 | --------------------------------------------------------------------------------