├── test ├── mocks │ ├── test │ │ ├── GET--a=b.mock │ │ └── GET.mock │ ├── GET.mock │ ├── import │ │ ├── data.json │ │ ├── GET.mock │ │ └── GET--around=true.mock │ ├── wildcard │ │ ├── exact │ │ │ └── GET.mock │ │ └── __ │ │ │ └── GET.mock │ ├── importjs │ │ ├── script.js │ │ ├── GET.mock │ │ ├── POST.mock │ │ └── scriptPost.js │ ├── wildcard-extended │ │ └── __ │ │ │ └── foobar │ │ │ ├── GET.mock │ │ │ └── __ │ │ │ └── fizzbuzz │ │ │ └── GET.mock │ ├── test1 │ │ └── test2 │ │ │ └── GET.mock │ ├── response-delay │ │ ├── GET--Delay=False.mock │ │ ├── GET--Delay=True.mock │ │ └── Get--Delay=Invalid.mock │ ├── return-empty-body │ │ └── GET.mock │ ├── request-headers │ │ ├── GET.mock │ │ ├── GET_Authorization=1234.mock │ │ ├── PUT_X-Foo=Baz.mock │ │ ├── GET_Authorization=5678.mock │ │ ├── PUT_Authorization=12.mock │ │ ├── PUT_Authorization=12_X-Foo=Bar.mock │ │ ├── PUT_X-Foo=Baz_Authorization=78.mock │ │ └── POST_Authorization=12_X-Foo=Bar--a=b.mock │ ├── return-200 │ │ ├── POST--Hello=123.mock │ │ └── POST.mock │ ├── eval │ │ └── GET.mock │ ├── headerimportjs │ │ ├── POST.mock │ │ ├── code.js │ │ └── scriptPost.js │ ├── dynamic-headers │ │ └── GET.mock │ ├── keep-line-feeds │ │ └── GET.mock │ ├── multiple-headers-same-name │ │ └── GET.mock │ ├── hello-world │ │ └── GET.mock │ ├── return-204 │ │ └── GET.mock │ └── multiple-headers │ │ └── GET.mock └── mockserver.js ├── .gitignore ├── .travis.yml ├── bin ├── images │ └── example-response.png └── mockserver.js ├── handlers ├── evalHandler.js ├── headerHandler.js └── importHandler.js ├── monad.js ├── package.json ├── README.md └── mockserver.js /test/mocks/test/GET--a=b.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK -------------------------------------------------------------------------------- /test/mocks/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | 3 | homepage -------------------------------------------------------------------------------- /test/mocks/import/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } -------------------------------------------------------------------------------- /test/mocks/wildcard/exact/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | 3 | more specific 4 | -------------------------------------------------------------------------------- /test/mocks/importjs/script.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "date": new Date() 3 | }; -------------------------------------------------------------------------------- /test/mocks/test/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | Welcome! -------------------------------------------------------------------------------- /test/mocks/wildcard/__/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | 3 | this always comes up 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | test.js 4 | .nyc_output 5 | *.code-workspace 6 | -------------------------------------------------------------------------------- /test/mocks/wildcard-extended/__/foobar/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | 3 | wildcards-extended -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '9' 5 | - '10' 6 | - '12' 7 | -------------------------------------------------------------------------------- /test/mocks/test1/test2/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | multi-level url -------------------------------------------------------------------------------- /test/mocks/response-delay/GET--Delay=False.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | false -------------------------------------------------------------------------------- /test/mocks/import/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | #import './data.json'; -------------------------------------------------------------------------------- /test/mocks/return-empty-body/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 204 OK 2 | Content-Type: text/xml; charset=utf-8 3 | 4 | -------------------------------------------------------------------------------- /test/mocks/importjs/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | #import './script.js'; -------------------------------------------------------------------------------- /test/mocks/request-headers/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 401 NOT AUTHORIZED 2 | Content-Type: text 3 | 4 | not authorized -------------------------------------------------------------------------------- /test/mocks/request-headers/GET_Authorization=1234.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | authorized -------------------------------------------------------------------------------- /test/mocks/request-headers/PUT_X-Foo=Baz.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | header x-foo only -------------------------------------------------------------------------------- /test/mocks/wildcard-extended/__/foobar/__/fizzbuzz/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | 3 | wildcards-extended-multiple -------------------------------------------------------------------------------- /bin/images/example-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namshi/mockserver/HEAD/bin/images/example-response.png -------------------------------------------------------------------------------- /test/mocks/importjs/POST.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | #import './scriptPost.js'; -------------------------------------------------------------------------------- /test/mocks/request-headers/GET_Authorization=5678.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | admin authorized -------------------------------------------------------------------------------- /test/mocks/request-headers/PUT_Authorization=12.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | header auth only -------------------------------------------------------------------------------- /test/mocks/return-200/POST--Hello=123.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/xml; charset=utf-8 3 | 4 | Hella -------------------------------------------------------------------------------- /test/mocks/importjs/scriptPost.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prop: request.body.indexOf('foo') !== -1 ? 'bar' : 'baz' 3 | }; -------------------------------------------------------------------------------- /test/mocks/request-headers/PUT_Authorization=12_X-Foo=Bar.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | header both -------------------------------------------------------------------------------- /test/mocks/response-delay/GET--Delay=True.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | Response-Delay: 1 4 | 5 | true -------------------------------------------------------------------------------- /test/mocks/eval/GET.mock: -------------------------------------------------------------------------------- 1 | #eval `HTTP/1.1 200 OK`; 2 | Content-Type: application/json 3 | 4 | #eval JSON.stringify({foo: 'bar'}); 5 | -------------------------------------------------------------------------------- /test/mocks/headerimportjs/POST.mock: -------------------------------------------------------------------------------- 1 | #import './code.js'; 2 | Content-Type: application/json 3 | 4 | #import './scriptPost.js'; 5 | -------------------------------------------------------------------------------- /test/mocks/response-delay/Get--Delay=Invalid.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | Response-Delay: Invalid 4 | 5 | invalid -------------------------------------------------------------------------------- /test/mocks/headerimportjs/code.js: -------------------------------------------------------------------------------- 1 | module.exports = request.body.indexOf('foo') !== -1 ? 'HTTP/1.1 200 OK' : 'HTTP/1.1 400 Bad request' 2 | -------------------------------------------------------------------------------- /test/mocks/headerimportjs/scriptPost.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prop: request.body.indexOf('foo') !== -1 ? 'bar' : 'baz' 3 | }; 4 | -------------------------------------------------------------------------------- /test/mocks/request-headers/PUT_X-Foo=Baz_Authorization=78.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | header both out-of-order -------------------------------------------------------------------------------- /test/mocks/request-headers/POST_Authorization=12_X-Foo=Bar--a=b.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text 3 | 4 | that is a long filename -------------------------------------------------------------------------------- /test/mocks/import/GET--around=true.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | 4 | stuff 5 | #import './data.json'; 6 | around me -------------------------------------------------------------------------------- /test/mocks/dynamic-headers/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: application/json 3 | X-Subject-Token: #header ${new Date()}; 4 | 5 | dynamic headers 6 | -------------------------------------------------------------------------------- /test/mocks/keep-line-feeds/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/plain; charset=utf-8 3 | 4 | ColumnA ColumnB ColumnC 5 | A1 B1 C1 6 | A2 B2 C2 7 | A3 B3 C3 8 | -------------------------------------------------------------------------------- /test/mocks/multiple-headers-same-name/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/xml; charset=utf-8 3 | Cache-Control: public, max-age=300 4 | Set-Cookie: PHPSESSID=abc; path=/ 5 | Set-Cookie: PHPSESSID=def; path=/ 6 | Set-Cookie: PHPSESSID=ghi; path=/ 7 | 8 | Welcome! -------------------------------------------------------------------------------- /handlers/evalHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = function evalHandler(value, request) { 2 | if (!/^#eval/m.test(value)) return value; 3 | return value 4 | .replace(/^#eval (.*);/m, function (statement, val) { 5 | return eval(val); 6 | }) 7 | .replace(/\r\n?/g, '\n'); 8 | } 9 | -------------------------------------------------------------------------------- /test/mocks/hello-world/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/xml; charset=utf-8 3 | 4 | { 5 | "Accept-Language": "en-US,en;q=0.8", 6 | "Host": "headers.jsontest.com", 7 | "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", 8 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 9 | } -------------------------------------------------------------------------------- /test/mocks/return-200/POST.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/xml; charset=utf-8 3 | 4 | { 5 | "Accept-Language": "en-US,en;q=0.8", 6 | "Host": "headers.jsontest.com", 7 | "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", 8 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 9 | } -------------------------------------------------------------------------------- /test/mocks/return-204/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 204 OK 2 | Content-Type: text/xml; charset=utf-8 3 | 4 | { 5 | "Accept-Language": "en-US,en;q=0.8", 6 | "Host": "headers.jsontest.com", 7 | "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", 8 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 9 | } -------------------------------------------------------------------------------- /handlers/headerHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = function headerHandler(value, request) { 2 | if (!/^#header/m.test(value)) return value; 3 | return value 4 | .replace(/^#header (.*);/m, function (statement, val) { 5 | const expression = val.replace(/[${}]/g, ''); 6 | return eval(expression); 7 | }) 8 | .replace(/\r\n?/g, '\n'); 9 | } 10 | -------------------------------------------------------------------------------- /test/mocks/multiple-headers/GET.mock: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/xml; charset=utf-8 3 | Cache-Control: public, max-age=300 4 | 5 | { 6 | "Accept-Language": "en-US,en;q=0.8", 7 | "Host": "headers.jsontest.com", 8 | "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", 9 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 10 | } -------------------------------------------------------------------------------- /monad.js: -------------------------------------------------------------------------------- 1 | function Monad(val) { 2 | this.__value = val; 3 | this.map = function map(f) { 4 | return Monad.of(f(this.__value)); 5 | } 6 | this.join = function join() { 7 | return this.__value; 8 | } 9 | this.chain = function chain(f) { 10 | return this.map(f).join(); 11 | } 12 | } 13 | Monad.of = function(val) { 14 | return new Monad(val); 15 | } 16 | 17 | 18 | module.exports = Monad; 19 | -------------------------------------------------------------------------------- /handlers/importHandler.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = function importHandler(value, context, request) { 5 | if (!/^#import/m.test(value)) return value; 6 | 7 | return value 8 | .replace(/^#import (.*);/m, function (includeStatement, file) { 9 | const importThisFile = file.replace(/['"]/g, ''); 10 | const content = fs.readFileSync(path.join(context, importThisFile)); 11 | if (importThisFile.endsWith('.js')) { 12 | return JSON.stringify(eval(content.toString())); 13 | } else { 14 | return content; 15 | } 16 | }) 17 | .replace(/\r\n?/g, '\n'); 18 | } 19 | -------------------------------------------------------------------------------- /bin/mockserver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var http = require('http'); 4 | var mockserver = require('./../mockserver'); 5 | var argv = require('yargs').argv; 6 | var colors = require('colors') 7 | var info = require('./../package.json'); 8 | var mocks = argv.m || argv.mocks; 9 | var port = argv.p || argv.port; 10 | var verbose = !(argv.q || argv.quiet); 11 | 12 | if (!mocks || !port) { 13 | console.log([ 14 | "Mockserver v" + info.version, 15 | "", 16 | "Usage:", 17 | " mockserver [-q] -p PORT -m PATH", 18 | "", 19 | "Options:", 20 | " -p, --port=PORT - Port to listen on", 21 | " -m, --mocks=PATH - Path to mock files", 22 | " -q, --quiet - Do not output anything", 23 | "", 24 | "Example:", 25 | " mockserver -p 8080 -m './mocks'" 26 | ].join("\n")); 27 | } else { 28 | http.createServer(mockserver(mocks, verbose)).listen(port); 29 | 30 | if (verbose) { 31 | console.log('Mockserver serving mocks {' 32 | + 'verbose'.yellow + ':' + (verbose && 'true'.green || 'false') 33 | + '} under "' + mocks.green + '" at ' 34 | + 'http://localhost:'.green + port.toString().green); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockserver", 3 | "version": "3.1.1", 4 | "description": "Easily mock your webservices while testing frontends.", 5 | "main": "mockserver.js", 6 | "scripts": { 7 | "test": "mocha -b", 8 | "coverage": "nyc mocha", 9 | "clean-install": "npm cache clean --force && rm -Rf node_modules && npm install" 10 | }, 11 | "bin": { 12 | "mockserver": "bin/mockserver.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/namshi/mockserver" 17 | }, 18 | "keywords": [ 19 | "test", 20 | "mock", 21 | "api", 22 | "webservice", 23 | "rest", 24 | "namshi" 25 | ], 26 | "author": "Mohammad Hallal ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/namshi/mockserver/issues" 30 | }, 31 | "homepage": "https://github.com/namshi/mockserver", 32 | "dependencies": { 33 | "colors": "^1.3.2", 34 | "header-case-normalizer": "^1.0.3", 35 | "js-combinatorics": "^0.5.0", 36 | "yargs": "^12.0.1" 37 | }, 38 | "devDependencies": { 39 | "mocha": "^5.2.0", 40 | "mock-req": "^0.2.0", 41 | "nyc": "^14.1.1" 42 | }, 43 | "engines": { 44 | "node": ">=8.10.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | This project is not under active maintenance (see https://github.com/namshi/mockserver/issues/82) 4 | 5 | Development is continued at https://github.com/fauxauldrich/camouflage 6 | 7 | For details on how to port your existing projects please visit [Camouflage Documentation](https://fauxauldrich.github.io/camouflage), or post your questions in Camouflage's [discussion](https://github.com/fauxauldrich/camouflage/discussions/22) 8 | 9 | # mockserver 10 | 11 | [![Build Status](https://travis-ci.org/namshi/mockserver.svg?branch=master)](https://travis-ci.org/namshi/mockserver) 12 | 13 | **mockserver** is a library that will help you mocking your APIs 14 | in **a matter of seconds**: you simply organize your mocked 15 | HTTP responses in a bunch of mock files and it will serve them 16 | like they were coming from a real API; in this way you can 17 | write your frontends without caring too much whether your 18 | backend is really ready or not. 19 | 20 | ## Installation 21 | 22 | Mockserver can be installed globally if you need 23 | to run it as a command: 24 | 25 | ``` 26 | $ npm install -g mockserver 27 | 28 | $ mockserver -p 8080 -m test/mocks 29 | Mockserver serving mocks under "test/mocks" at http://localhost:8080 30 | ``` 31 | 32 | or as a regular NPM module if you need to use it as 33 | a library within your code: 34 | 35 | ```bash 36 | npm install mockserver 37 | ``` 38 | 39 | then in your test file: 40 | 41 | ```javascript 42 | var http = require('http'); 43 | var mockserver = require('mockserver'); 44 | 45 | http.createServer(mockserver('path/to/your/mocks')).listen(9001); 46 | ``` 47 | 48 | This will run a simple HTTP webserver, handled by mockserver, on port 9001. 49 | 50 | At this point you can simply define your first mock: create a file in 51 | `path/to/your/mocks/example-response` called `GET.mock`: 52 | 53 | ``` 54 | HTTP/1.1 200 OK 55 | Content-Type: application/json; charset=utf-8 56 | 57 | { 58 | "Random": "content" 59 | } 60 | ``` 61 | 62 | If you open your browser at `http://localhost:9001/example-response` 63 | you will see something like this: 64 | 65 | ![example output](https://raw.githubusercontent.com/namshi/mockserver/readme/bin/images/example-response.png) 66 | 67 | And it's over: now you can start writing your frontends without 68 | having to wait for your APIs to be ready, or without having to spend 69 | too much time mocking them, as mockserver lets you do it in seconds. 70 | 71 | ## Verbosity 72 | 73 | By default mockserver is running in verbose mode: log messages are pushed to `stdout`. 74 | That will help to distinguish, which mock file matches best the request. 75 | 76 | ```shell 77 | $ mockserver -p 8080 -m './mocks' 78 | Mockserver serving mocks {verbose:true} under "./mocks" at http://localhost:8080 79 | Reading from ./mocks/api/GET--a=b.mock file: Not matched 80 | Reading from ./mocks/api/GET.mock file: Matched 81 | ``` 82 | 83 | Option `-q|--quiet` disables this behavior. 84 | 85 | ## Mock files 86 | 87 | As you probably understood, mock files' naming conventions are based 88 | on the response that they are going to serve: 89 | 90 | ``` 91 | $REQUEST-PATH/$HTTP-METHOD.mock 92 | ``` 93 | 94 | For example, let's say that you wanna mock the response of a POST request 95 | to `/users`, you would simply need to create a file named `POST.mock` under `users/`. 96 | 97 | The content of the mock files needs to be a valid HTTP response, for example: 98 | 99 | ``` 100 | HTTP/1.1 200 OK 101 | Content-Type: text/xml; charset=utf-8 102 | 103 | { 104 | "Accept-Language": "en-US,en;q=0.8", 105 | "Host": "headers.jsontest.com", 106 | "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3", 107 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 108 | } 109 | ``` 110 | 111 | Check [our own mocks](https://github.com/namshi/mockserver/tree/master/test/mocks) as a reference. 112 | 113 | ## Custom Headers 114 | 115 | You can specify request headers to include, which allows you to change the response based on what headers are 116 | provided. 117 | 118 | To do this, you need to let mockserver know which headers matter, 119 | by exposing comma-separated environment `MOCK_HEADERS` variable, like so: 120 | 121 | ```shell 122 | $ MOCK_HEADERS=x-foo,authorization mockserver -m . -p 9001 123 | ``` 124 | 125 | Or by setting the `headers` array on the mockserver object, like so: 126 | 127 | ```js 128 | var mockserver = require('mockserver'); 129 | mockserver.headers = ['Authorization', 'X-My-Header']; 130 | ``` 131 | 132 | Any headers that are set and occur within the array will now be appended to the filename, immediately after the 133 | HTTP method, like so: 134 | 135 | ``` 136 | GET /hello 137 | Authorization: 12345 138 | 139 | hello/GET_Authorization=12345.mock 140 | ``` 141 | 142 | ``` 143 | GET /hello 144 | X-My-Header: cow 145 | Authorization: 12345 146 | 147 | hello/GET_Authorization=12345_X-My-Header=cow.mock 148 | ``` 149 | 150 | **Note:** The order of the headers within the `headers` array determines the order of the values within the filename. 151 | 152 | The server will always attempt to match the file with the most tracked headers, then it will try permutations of 153 | headers until it finds one that matches. This means that, in the previous example, the server will look for files 154 | in this order: 155 | 156 | ``` 157 | hello/GET_Authorization=12345_X-My-Header=cow.mock 158 | hello/GET_X-My-Header_Authorization=12345=cow.mock 159 | hello/GET_Authorization=12345.mock 160 | hello/GET_X-My-Header=cow.mock 161 | hello/GET.mock 162 | ``` 163 | 164 | The first one matched is the one returned, favoring more matches and headers earlier in the array. 165 | 166 | The `headers` array can be set or modified at any time. 167 | 168 | ## Response Delays 169 | 170 | When building applications, we cannot always guarantee that our users have a fast connection, which 171 | is latency free. Also some HTTP calls inevitably take more time than we'd, like so we have added 172 | the ability to simulate HTTP call latency by setting a custom header 173 | 174 | ``` 175 | Response-Delay: 5000 176 | ``` 177 | 178 | The delay value is expected in milliseconds, if not set for a given file there will be no delay. 179 | 180 | ## Query string parameters and POST body 181 | 182 | In order to support query string parameters in the mocked files, replace all occurrences of `?` with `--`, then 183 | append the entire string to the end of the file. 184 | 185 | ``` 186 | GET /hello?a=b 187 | 188 | hello/GET--a=b.mock 189 | ``` 190 | 191 | ``` 192 | GET /test?a=b&c=d? 193 | 194 | test/GET--a=b&c=d--.mock 195 | ``` 196 | 197 | (This has been introduced to overcome issues in file naming on windows) 198 | 199 | To combine custom headers and query parameters, simply add the headers _then_ add the parameters: 200 | 201 | ``` 202 | GET /hello?a=b 203 | Authorization: 12345 204 | 205 | hello/GET_Authorization=12345--a=b.mock 206 | ``` 207 | 208 | Similarly, you can do the same thing with the body of a POST request: 209 | if you send `Hello=World` as body of the request, mockserver will 210 | look for a file called `POST--Hello=World.mock` 211 | 212 | In the same way, if your POST body is a json like `{"json": "yesPlease"}`, 213 | mockserver will look for a file called `POST--{"json": "yesPlease"}.mock`. 214 | _Warning! This feature is_ **NOT compatible with Windows**_. This is because Windows doesn't accept curly brackets as filenames._ 215 | 216 | If no parametrized mock file is found, mockserver will default to the 217 | nearest headers based .mock file 218 | 219 | ex: 220 | 221 | ``` 222 | GET /hello?a=b 223 | Authorization: 12345 224 | ``` 225 | 226 | if there's no `hello/GET_Authorization=12345--a=b.mock`, we'll default to `hello/GET_Authorization=12345.mock` or to `hello/GET.mock` 227 | 228 | ## Wildcard slugs 229 | 230 | If you want to match against a route with a wildcard - say in the case of an ID or other parameter in the URL, you can 231 | create a directory named `__` as a wildcard. 232 | 233 | For example, let's say that you want mock the response of a GET request 234 | to `/users/:id`, you can create files named `users/1/GET.mock`, `users/2/GET.mock`, `users/3/GET.mock`, etc. 235 | 236 | Then to create one catchall, you can create another file `users/__/GET.mock`. This file will act as a fallback 237 | for any other requests: 238 | 239 | ex: 240 | 241 | ``` 242 | GET /users/2 243 | 244 | GET /users/2/GET.mock 245 | ``` 246 | 247 | ex: 248 | 249 | ``` 250 | GET /users/1000 251 | 252 | GET /users/__/GET.mock 253 | ``` 254 | 255 | ex: 256 | 257 | ``` 258 | GET /users/1000/detail 259 | 260 | GET /users/__/detail/GET.mock 261 | ``` 262 | 263 | ## Custom imports 264 | 265 | Say you have some json you want to use in your unit tests, and also serve as the body of the call. You can use this import syntax: 266 | 267 | ``` 268 | HTTP/1.1 200 OK 269 | Content-Type: application/json 270 | 271 | #import './data.json'; 272 | ``` 273 | 274 | whereby `./data.json` is a file relative to the including mock file. You can have as many imports as you want per mock file. 275 | 276 | You can also import `javascript` modules to create dynamic responses: 277 | 278 | ```js 279 | // script.js 280 | module.exports = { 281 | id: Math.random() 282 | .toString(36) 283 | .substring(7), 284 | date: new Date(), 285 | }; 286 | ``` 287 | 288 | Then import the file as above `#import './script.js'` 289 | 290 | Dynamic values of headers can be filled with valid JS statements such as: 291 | 292 | ``` 293 | X-Subject-Token: #header ${require('uuid/v4')()}; 294 | ``` 295 | 296 | ## Custom response status 297 | 298 | You can specify response status (200, 201, 404. etc.) depending on request parameters. To do this, you need to use `#import './code.js';` in first line of your mock file: 299 | 300 | ``` 301 | #import './code.js'; 302 | Content-Type: application/json; charset=utf-8 303 | Access-Control-Allow-Origin: * 304 | 305 | { 306 | "Random": "Content" 307 | } 308 | ``` 309 | 310 | You import `javascript` modules to create dynamic code responses: 311 | 312 | ```js 313 | // code.js 314 | module.exports = request.body.indexOf('foo') !== -1 ? 'HTTP/1.1 200 OK' : 'HTTP/1.1 400 Bad request' 315 | ``` 316 | 317 | ## Tests 318 | 319 | Tests run on travis, but if you wanna run them locally you simply 320 | have to run `mocha` or its verbose cousin `./node_modules/mocha/bin/mocha` 321 | (if you don't have mocha installed globally). 322 | 323 | To run test with debug output, expose `DEBUG=true` environment variable: 324 | 325 | ```shell 326 | $ DEBUG=true ./node_modules/mocha/bin/mocha 327 | ``` 328 | 329 | Or as npm shortcut: 330 | 331 | ```shell 332 | $ DEBUG=true npm test 333 | ``` 334 | -------------------------------------------------------------------------------- /mockserver.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const colors = require('colors'); 4 | const join = path.join; 5 | const Combinatorics = require('js-combinatorics'); 6 | const normalizeHeader = require('header-case-normalizer'); 7 | const Monad = require('./monad'); 8 | const importHandler = require('./handlers/importHandler'); 9 | const headerHandler = require('./handlers/headerHandler'); 10 | const evalHandler = require('./handlers/evalHandler'); 11 | /** 12 | * Returns the status code out of the 13 | * first line of an HTTP response 14 | * (ie. HTTP/1.1 200 Ok) 15 | */ 16 | function parseStatus(header) { 17 | const regex = /(?<=HTTP\/\d.\d\s{1,1})(\d{3,3})(?=[a-z0-9\s]+)/gi; 18 | if (!regex.test(header)) throw new Error('Response code should be valid string'); 19 | 20 | const res = header.match(regex); 21 | return res.join(''); 22 | } 23 | 24 | /** 25 | * Parses an HTTP header, splitting 26 | * by colon. 27 | */ 28 | const parseHeader = function (header, context, request) { 29 | header = header.split(': '); 30 | 31 | return { key: normalizeHeader(header[0]), value: parseValue(header[1], context, request) }; 32 | }; 33 | 34 | const parseValue = function(value, context, request) { 35 | return Monad 36 | .of(value) 37 | .map((value) => importHandler(value, context, request)) 38 | .map((value) => headerHandler(value, request)) 39 | .map((value) => evalHandler(value, request)) 40 | .join(); 41 | }; 42 | 43 | /** 44 | * Prepares headers to watch, no duplicates, non-blanks. 45 | * Priority exports over ENV definition. 46 | */ 47 | const prepareWatchedHeaders = function() { 48 | const exportHeaders = 49 | module.exports.headers && module.exports.headers.toString(); 50 | const headers = (exportHeaders || process.env.MOCK_HEADERS || '').split(','); 51 | 52 | return headers.filter(function(item, pos, self) { 53 | return item && self.indexOf(item) == pos; 54 | }); 55 | }; 56 | 57 | /** 58 | * Combining the identically named headers 59 | */ 60 | const addHeader = function(headers, line) { 61 | const { key, value } = parseHeader(line); 62 | 63 | if (headers[key]) { 64 | headers[key] = [...(Array.isArray(headers[key]) ? headers[key] : [headers[key]]), value]; 65 | } else { 66 | headers[key] = value; 67 | } 68 | } 69 | 70 | /** 71 | * Parser the content of a mockfile 72 | * returning an HTTP-ish object with 73 | * status code, headers and body. 74 | */ 75 | const parse = function(content, file, request) { 76 | const context = path.parse(file).dir + '/'; 77 | const headers = {}; 78 | let body; 79 | const bodyContent = []; 80 | content = content.split(/\r?\n/); 81 | const status = Monad 82 | .of(content[0]) 83 | .map((value) => importHandler(value, context, request)) 84 | .map((value) => evalHandler(value, context, request)) 85 | .map(parseStatus) 86 | .join(); 87 | 88 | 89 | let headerEnd = false; 90 | delete content[0]; 91 | 92 | content.forEach(function(line) { 93 | switch (true) { 94 | case headerEnd: 95 | bodyContent.push(line); 96 | break; 97 | case line === '' || line === '\r': 98 | headerEnd = true; 99 | break; 100 | default: 101 | addHeader(headers, line); 102 | break; 103 | } 104 | }); 105 | 106 | 107 | body = Monad 108 | .of(bodyContent.join('\n')) 109 | .map((value) => importHandler(value, context, request)) 110 | .map((value) => evalHandler(value, context, request)) 111 | .join(); 112 | 113 | return { status: status, headers: headers, body: body }; 114 | }; 115 | 116 | function removeBlanks(array) { 117 | return array.filter(function(i) { 118 | return i; 119 | }); 120 | } 121 | 122 | 123 | /** 124 | * This method will look for a header named Response-Delay. When set it 125 | * delay the response in that number of milliseconds simulating latency 126 | * for HTTP calls. 127 | * 128 | * Example from a file: 129 | * Response-Delay: 5000 130 | * 131 | * @param {mock.headers} headers : { 132 | * 'Response-Delay': is the property name, 133 | * 'value': Positive integer value 134 | */ 135 | const getResponseDelay = function(headers) { 136 | if (headers && headers.hasOwnProperty('Response-Delay')) { 137 | let delayVal = parseInt(headers['Response-Delay'], 10); 138 | delayVal = isNaN(delayVal) || delayVal < 0 ? 0 : delayVal; 139 | return delayVal; 140 | } 141 | return 0; 142 | }; 143 | 144 | function getWildcardPath(dir) { 145 | let steps = removeBlanks(dir.split('/')); 146 | let testPath; 147 | let newPath; 148 | let exists = false; 149 | 150 | while (steps.length) { 151 | steps.pop(); 152 | testPath = join(steps.join('/'), '/__'); 153 | exists = fs.existsSync(join(mockserver.directory, testPath)); 154 | if (exists) { 155 | newPath = testPath; 156 | } 157 | } 158 | 159 | const res = getDirectoriesRecursive(mockserver.directory) 160 | .filter(dir => { 161 | const directories = dir.split(path.sep); 162 | return directories.includes('__'); 163 | }) 164 | .sort((a, b) => { 165 | const aLength = a.split(path.sep); 166 | const bLength = b.split(path.sep); 167 | 168 | if (aLength == bLength) return 0; 169 | 170 | // Order from longest file path to shortest. 171 | return aLength > bLength ? -1 : 1; 172 | }) 173 | .map(dir => { 174 | const steps = dir.split(path.sep); 175 | const baseDir = mockserver.directory.split(path.sep); 176 | steps.splice(0, baseDir.length); 177 | return steps.join(path.sep); 178 | }); 179 | 180 | steps = removeBlanks(dir.split('/')); 181 | 182 | newPath = matchWildcardPaths(res, steps) || newPath; 183 | 184 | return newPath; 185 | } 186 | 187 | function matchWildcardPaths(res, steps) { 188 | for (let resIndex = 0; resIndex < res.length; resIndex++) { 189 | const dirSteps = res[resIndex].split(/\/|\\/); 190 | if (dirSteps.length !== steps.length) { 191 | continue; 192 | } 193 | const result = matchWildcardPath(steps, dirSteps); 194 | if (result) { 195 | return result; 196 | } 197 | } 198 | return null; 199 | } 200 | 201 | function matchWildcardPath(steps, dirSteps) { 202 | for (let stepIndex = 1; stepIndex <= steps.length; stepIndex++) { 203 | const step = steps[steps.length - stepIndex]; 204 | const dirStep = dirSteps[dirSteps.length - stepIndex]; 205 | if (step !== dirStep && dirStep != '__') { 206 | return null; 207 | } 208 | } 209 | return '/' + dirSteps.join('/'); 210 | } 211 | 212 | function flattenDeep(directories) { 213 | return directories.reduce( 214 | (acc, val) => 215 | Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), 216 | [] 217 | ); 218 | } 219 | 220 | function getDirectories(srcpath) { 221 | return fs 222 | .readdirSync(srcpath) 223 | .map(file => path.join(srcpath, file)) 224 | .filter(path => fs.statSync(path).isDirectory()); 225 | } 226 | 227 | function getDirectoriesRecursive(srcpath) { 228 | const nestedDirectories = getDirectories(srcpath).map( 229 | getDirectoriesRecursive 230 | ); 231 | const directories = flattenDeep(nestedDirectories); 232 | directories.push(srcpath); 233 | return directories; 234 | } 235 | 236 | /** 237 | * Returns the body or query string to be used in 238 | * the mock name. 239 | * 240 | * In any case we will prepend the value with a double 241 | * dash so that the mock files will look like: 242 | * 243 | * POST--My-Body=123.mock 244 | * 245 | * or 246 | * 247 | * GET--query=string&hello=hella.mock 248 | */ 249 | function getBodyOrQueryString(body, query) { 250 | if (query) { 251 | return '--' + query; 252 | } 253 | 254 | if (body && body !== '') { 255 | return '--' + body; 256 | } 257 | 258 | return body; 259 | } 260 | 261 | /** 262 | * Ghetto way to get the body 263 | * out of the request. 264 | * 265 | * There are definitely better 266 | * ways to do this (ie. npm/body 267 | * or npm/body-parser) but for 268 | * the time being this does it's work 269 | * (ie. we don't need to support 270 | * fancy body parsing in mockserver 271 | * for now). 272 | */ 273 | function getBody(req, callback) { 274 | let body = ''; 275 | 276 | req.on('data', function(b) { 277 | body = body + b.toString(); 278 | }); 279 | 280 | req.on('end', function() { 281 | callback(body); 282 | }); 283 | } 284 | 285 | function getMockedContent(path, prefix, body, query) { 286 | const mockName = prefix + (getBodyOrQueryString(body, query) || '') + '.mock'; 287 | const mockFile = join(mockserver.directory, path, mockName); 288 | let content; 289 | 290 | try { 291 | content = fs.readFileSync(mockFile, { encoding: 'utf8' }); 292 | if (mockserver.verbose) { 293 | console.log( 294 | 'Reading from ' + mockFile.yellow + ' file: ' + 'Matched'.green 295 | ); 296 | } 297 | } catch (err) { 298 | if (mockserver.verbose) { 299 | console.log( 300 | 'Reading from ' + mockFile.yellow + ' file: ' + 'Not matched'.red 301 | ); 302 | } 303 | content = (body || query) && getMockedContent(path, prefix); 304 | } 305 | 306 | return content; 307 | } 308 | 309 | function getContentFromPermutations(path, method, body, query, permutations) { 310 | let content, prefix; 311 | 312 | while (permutations.length) { 313 | prefix = method + permutations.pop().join(''); 314 | content = getMockedContent(path, prefix, body, query) || content; 315 | } 316 | 317 | return { content: content, prefix: prefix }; 318 | } 319 | 320 | const mockserver = { 321 | directory: '.', 322 | verbose: false, 323 | headers: [], 324 | init: function(directory, verbose) { 325 | this.directory = directory; 326 | this.verbose = !!verbose; 327 | this.headers = prepareWatchedHeaders(); 328 | }, 329 | handle: function(req, res) { 330 | getBody(req, function(body) { 331 | req.body = body; 332 | const url = req.url; 333 | let path = url; 334 | 335 | const queryIndex = url.indexOf('?'), 336 | query = 337 | queryIndex >= 0 ? url.substring(queryIndex).replace(/\?/g, '') : '', 338 | method = req.method.toUpperCase(), 339 | headers = []; 340 | 341 | if (queryIndex > 0) { 342 | path = url.substring(0, queryIndex); 343 | } 344 | 345 | if (req.headers && mockserver.headers.length) { 346 | mockserver.headers.forEach(function(header) { 347 | header = header.toLowerCase(); 348 | if (req.headers[header]) { 349 | headers.push( 350 | '_' + normalizeHeader(header) + '=' + req.headers[header] 351 | ); 352 | } 353 | }); 354 | } 355 | 356 | // Now, permute the possible headers, and look for any matching files, prioritizing on 357 | // both # of headers and the original header order 358 | let matched, 359 | permutations = [[]]; 360 | 361 | if (headers.length) { 362 | permutations = Combinatorics.permutationCombination(headers) 363 | .toArray() 364 | .sort(function(a, b) { 365 | return b.length - a.length; 366 | }); 367 | permutations.push([]); 368 | } 369 | 370 | matched = getContentFromPermutations( 371 | path, 372 | method, 373 | body, 374 | query, 375 | permutations.slice(0) 376 | ); 377 | 378 | if (!matched.content && (path = getWildcardPath(path))) { 379 | matched = getContentFromPermutations( 380 | path, 381 | method, 382 | body, 383 | query, 384 | permutations.slice(0) 385 | ); 386 | } 387 | 388 | if (matched.content) { 389 | const mock = parse( 390 | matched.content, 391 | join(mockserver.directory, path, matched.prefix), 392 | req 393 | ); 394 | const delay = getResponseDelay(mock.headers); 395 | Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay); 396 | res.writeHead(mock.status, mock.headers); 397 | return res.end(mock.body); 398 | } else { 399 | res.writeHead(404); 400 | res.end('Not Mocked'); 401 | } 402 | }); 403 | }, 404 | }; 405 | 406 | module.exports = function(directory, silent) { 407 | mockserver.init(directory, silent); 408 | 409 | return mockserver.handle; 410 | }; 411 | 412 | module.exports.headers = null; 413 | module.exports.getResponseDelay = getResponseDelay; 414 | -------------------------------------------------------------------------------- /test/mockserver.js: -------------------------------------------------------------------------------- 1 | const MockReq = require('mock-req'); 2 | const assert = require('assert'); 3 | const mockserver = require('./../mockserver'); 4 | const path = require('path'); 5 | const Monad = require('../monad'); 6 | 7 | let res; 8 | let req; 9 | const mocksDirectory = path.join('.', 'test', 'mocks'); 10 | 11 | const verbose = process.env.DEBUG === 'true' || false; 12 | 13 | /** 14 | * Processes request 15 | */ 16 | function processRequest(url, method) { 17 | req.url = url; 18 | req.method = method; 19 | mockserver(mocksDirectory, verbose)(req, res); 20 | } 21 | 22 | /** 23 | * Processes request within custom ENV 24 | */ 25 | function processRequestEnv(url, method, envs) { 26 | let cleanupEnv = function() {}; 27 | 28 | for (let name in envs) { 29 | if (envs.hasOwnProperty(name)) { 30 | process.env[name] = envs[name]; 31 | 32 | cleanupEnv = (function(name, next) { 33 | return function() { 34 | delete process.env[name]; 35 | next(); 36 | }; 37 | })(name, cleanupEnv); 38 | } 39 | } 40 | 41 | processRequest(url, method); 42 | 43 | cleanupEnv(); 44 | } 45 | 46 | describe('mockserver', function() { 47 | beforeEach(function() { 48 | mockserver.headers = []; 49 | 50 | res = { 51 | headers: null, 52 | status: null, 53 | body: null, 54 | writeHead: function(status, headers) { 55 | this.status = status; 56 | this.headers = headers; 57 | }, 58 | end: function(body) { 59 | this.body = body; 60 | }, 61 | }; 62 | 63 | req = { 64 | url: null, 65 | method: null, 66 | headers: [], 67 | on: function(event, cb) { 68 | if (event === 'end') { 69 | cb(); 70 | } 71 | }, 72 | }; 73 | }); 74 | 75 | describe('mockserver()', function() { 76 | it('should return a valid response', function() { 77 | processRequest('/test', 'GET'); 78 | 79 | assert.equal(res.body, 'Welcome!'); 80 | assert.equal(res.status, 200); 81 | assert.equal(JSON.stringify(res.headers), '{"Content-Type":"text"}'); 82 | }); 83 | 84 | it('should return 404 if the mock does not exist', function() { 85 | processRequest('/not-there', 'GET'); 86 | 87 | assert.equal(res.status, 404); 88 | assert.equal(res.body, 'Not Mocked'); 89 | }); 90 | 91 | it('should be able to handle trailing slashes without changing the name of the mockfile', function() { 92 | processRequest('/test/', 'GET'); 93 | 94 | assert.equal(res.status, 200); 95 | assert.equal(res.body, 'Welcome!'); 96 | assert.equal(JSON.stringify(res.headers), '{"Content-Type":"text"}'); 97 | }); 98 | 99 | it('should be able to handle multiple headers', function() { 100 | processRequest('/multiple-headers/', 'GET'); 101 | 102 | assert.equal(res.status, 200); 103 | assert.equal( 104 | JSON.stringify(res.headers), 105 | '{"Content-Type":"text/xml; charset=utf-8","Cache-Control":"public, max-age=300"}' 106 | ); 107 | }); 108 | 109 | it('should combine the identical headers names', function() { 110 | processRequest('/multiple-headers-same-name/', 'GET'); 111 | 112 | assert.equal(res.headers['Set-Cookie'].length, 3); 113 | }) 114 | 115 | it('should be able to handle status codes different than 200', function() { 116 | processRequest('/return-204', 'GET'); 117 | 118 | assert.equal(res.status, 204); 119 | }); 120 | 121 | it('should be able to handle HTTP methods other than GET', function() { 122 | processRequest('/return-200', 'POST'); 123 | 124 | assert.equal(res.status, 200); 125 | }); 126 | 127 | it('should be able to handle empty bodies', function() { 128 | processRequest('/return-empty-body', 'GET'); 129 | 130 | assert.equal(res.status, 204); 131 | assert.equal(res.body, ''); 132 | }); 133 | 134 | it('should be able to correctly map /', function() { 135 | processRequest('/', 'GET'); 136 | 137 | assert.equal(res.body, 'homepage'); 138 | }); 139 | 140 | it('should be able to map multi-level urls', function() { 141 | processRequest('/test1/test2', 'GET'); 142 | 143 | assert.equal(res.body, 'multi-level url'); 144 | }); 145 | 146 | it('should be able to handle GET parameters', function() { 147 | processRequest('/test?a=b', 'GET'); 148 | 149 | assert.equal(res.status, 200); 150 | }); 151 | 152 | it('should default to GET.mock if no matching parameter file is found', function() { 153 | processRequest('/test?a=c', 'GET'); 154 | 155 | assert.equal(res.status, 200); 156 | }); 157 | 158 | it('should be able track custom headers', function() { 159 | mockserver.headers = ['authorization']; 160 | 161 | processRequest('/request-headers', 'GET'); 162 | assert.equal(res.status, 401); 163 | assert.equal(res.body, 'not authorized'); 164 | 165 | req.headers['authorization'] = '1234'; 166 | processRequest('/request-headers', 'GET'); 167 | assert.equal(res.status, 200); 168 | assert.equal(res.body, 'authorized'); 169 | 170 | req.headers['authorization'] = '5678'; 171 | processRequest('/request-headers', 'GET'); 172 | assert.equal(res.status, 200); 173 | assert.equal(res.body, 'admin authorized'); 174 | }); 175 | 176 | it('should attempt to fall back to a base method if a custom header is not found in a file', function() { 177 | mockserver.headers = ['authorization']; 178 | 179 | req.headers['authorization'] = 'invalid'; 180 | processRequest('/request-headers', 'GET'); 181 | assert.equal(res.status, 401); 182 | assert.equal(res.body, 'not authorized'); 183 | 184 | req.headers['authorization'] = 'invalid'; 185 | processRequest('/request-headers', 'POST'); 186 | assert.equal(res.status, 404); 187 | assert.equal(res.body, 'Not Mocked'); 188 | }); 189 | 190 | it('should look for alternate combinations of headers if a custom header is not found', function() { 191 | mockserver.headers = ['authorization', 'x-foo']; 192 | 193 | req.headers['authorization'] = 12; 194 | req.headers['x-foo'] = 'Bar'; 195 | processRequest('/request-headers', 'PUT'); 196 | assert.equal(res.status, 200); 197 | assert.equal(res.body, 'header both'); 198 | 199 | req.headers['x-foo'] = 'Baz'; 200 | processRequest('/request-headers', 'PUT'); 201 | assert.equal(res.status, 200); 202 | assert.equal(res.body, 'header auth only'); 203 | 204 | req.headers['authorization'] = 78; 205 | processRequest('/request-headers', 'PUT'); 206 | assert.equal(res.status, 200); 207 | assert.equal(res.body, 'header both out-of-order'); 208 | 209 | req.headers['authorization'] = 45; 210 | processRequest('/request-headers', 'PUT'); 211 | assert.equal(res.status, 200); 212 | assert.equal(res.body, 'header x-foo only'); 213 | 214 | delete req.headers['authorization']; 215 | processRequest('/request-headers', 'PUT'); 216 | assert.equal(res.status, 200); 217 | assert.equal(res.body, 'header x-foo only'); 218 | }); 219 | 220 | it('should be able track custom headers with variation and query params', function() { 221 | mockserver.headers = ['authorization', 'x-foo']; 222 | req.headers['authorization'] = 12; 223 | req.headers['x-foo'] = 'Bar'; 224 | processRequest('/request-headers?a=b', 'POST'); 225 | assert.equal(res.status, 200); 226 | assert.equal(res.body, 'that is a long filename'); 227 | }); 228 | 229 | it('should be able track custom string headers with variation and query params', function() { 230 | mockserver.headers = 'authorization,x-foo'; 231 | 232 | req.headers['authorization'] = 12; 233 | req.headers['x-foo'] = 'Bar'; 234 | 235 | processRequest('/request-headers?a=b', 'POST'); 236 | 237 | assert.equal(res.status, 200); 238 | assert.equal(res.body, 'that is a long filename'); 239 | }); 240 | 241 | it('should be able track custom ENV headers with variation and query params', function() { 242 | req.headers['authorization'] = 12; 243 | req.headers['x-foo'] = 'Bar'; 244 | 245 | processRequestEnv('/request-headers?a=b', 'POST', { 246 | MOCK_HEADERS: 'authorization,x-foo', 247 | }); 248 | 249 | assert.equal(res.status, 200); 250 | assert.equal(res.body, 'that is a long filename'); 251 | }); 252 | 253 | it('should keep line feeds (U+000A)', function() { 254 | processRequest('/keep-line-feeds', 'GET'); 255 | 256 | assert.equal( 257 | res.body, 258 | 'ColumnA ColumnB ColumnC\n' + 'A1 B1 C1\n' + 'A2 B2 C2\n' + 'A3 B3 C3\n' 259 | ); 260 | assert.equal(res.status, 200); 261 | assert.equal( 262 | JSON.stringify(res.headers), 263 | '{"Content-Type":"text/plain; charset=utf-8"}' 264 | ); 265 | }); 266 | 267 | it('should be able to include POST bodies in the mock location', function(done) { 268 | const req = new MockReq({ 269 | method: 'POST', 270 | url: '/return-200', 271 | headers: { 272 | Accept: 'text/plain', 273 | }, 274 | }); 275 | req.write('Hello=123'); 276 | req.end(); 277 | 278 | mockserver(mocksDirectory, verbose)(req, res); 279 | 280 | req.on('end', function() { 281 | assert.equal(res.body, 'Hella'); 282 | assert.equal(res.status, 200); 283 | done(); 284 | }); 285 | }); 286 | 287 | it('Should default to POST.mock if no match for body is found', function(done) { 288 | const req = new MockReq({ 289 | method: 'POST', 290 | url: '/return-200', 291 | headers: { 292 | Accept: 'text/plain', 293 | }, 294 | }); 295 | req.write('Hello=456'); 296 | req.end(); 297 | 298 | mockserver(mocksDirectory, verbose)(req, res); 299 | 300 | req.on('end', function() { 301 | assert.equal(res.status, 200); 302 | done(); 303 | }); 304 | }); 305 | 306 | it('Should return 404 when no default .mock files are found', function() { 307 | mockserver.headers = ['authorization']; 308 | req.headers['authorization'] = 12; 309 | processRequest('/return-200?a=c', 'GET'); 310 | 311 | assert.equal(res.status, 404); 312 | }); 313 | 314 | it('should be able to handle imports', function() { 315 | processRequest('/import', 'GET'); 316 | 317 | assert.equal(res.status, 200); 318 | assert.equal(res.body, JSON.stringify({ foo: 'bar' }, null, 4)); 319 | }); 320 | 321 | it('should be able to handle eval', function() { 322 | processRequest('/eval', 'GET'); 323 | 324 | assert.equal(res.status, 200); 325 | assert.deepEqual(JSON.parse(res.body), { foo: 'bar' }); 326 | }); 327 | 328 | it('should be able to handle imports with content around import', function() { 329 | processRequest('/import?around=true', 'GET'); 330 | 331 | assert.equal(res.status, 200); 332 | assert.equal( 333 | res.body, 334 | 'stuff\n' + JSON.stringify({ foo: 'bar' }, null, 4) + '\naround me' 335 | ); 336 | }); 337 | 338 | it('should be able to handle imports with js scripts', function() { 339 | processRequest('/importjs', 'GET'); 340 | assert.equal(res.status, 200); 341 | assert.ok(Date.parse(JSON.parse(res.body).date)); 342 | }); 343 | 344 | it('should be able to handle imports with js scripts varying responses according to the the request - 1', function(done) { 345 | var req = new MockReq({ 346 | method: 'POST', 347 | url: '/importjs', 348 | headers: {}, 349 | }); 350 | req.write(JSON.stringify({ foo: '123' })); 351 | req.end(); 352 | 353 | mockserver(mocksDirectory, verbose)(req, res); 354 | 355 | req.on('end', function() { 356 | assert.equal(JSON.parse(res.body).prop, 'bar'); 357 | done(); 358 | }); 359 | }); 360 | 361 | it('should be able to handle imports with js scripts varying responses according to the the request - 2', function(done) { 362 | var req = new MockReq({ 363 | method: 'POST', 364 | url: '/importjs', 365 | headers: {}, 366 | }); 367 | req.write(JSON.stringify({ boo: '123' })); 368 | req.end(); 369 | 370 | mockserver(mocksDirectory, verbose)(req, res); 371 | 372 | req.on('end', function() { 373 | assert.equal(JSON.parse(res.body).prop, 'baz'); 374 | done(); 375 | }); 376 | }); 377 | 378 | it('should be able to handle dynamic header values', function() { 379 | processRequest('/dynamic-headers', 'GET'); 380 | assert.equal(res.status, 200); 381 | assert.ok(Date.parse(res.headers['X-Subject-Token'])); 382 | assert.equal(res.body, 'dynamic headers\n'); 383 | }); 384 | 385 | describe('wildcard directories', function() { 386 | it('wildcard matches directories named __ with numeric slug', function() { 387 | processRequest('/wildcard/123', 'GET'); 388 | 389 | assert.equal(res.status, 200); 390 | assert.equal(res.body, 'this always comes up\n'); 391 | }); 392 | 393 | it('wildcard matches directories named __ with string slug', function() { 394 | processRequest('/wildcard/abc', 'GET'); 395 | 396 | assert.equal(res.status, 200); 397 | assert.equal(res.body, 'this always comes up\n'); 398 | }); 399 | 400 | it('wildcard matches directories named foo/__/bar with numeric slug', function() { 401 | processRequest('/wildcard-extended/123/foobar', 'GET'); 402 | 403 | assert.equal(res.status, 200); 404 | assert.equal(res.body, 'wildcards-extended'); 405 | }); 406 | 407 | it('wildcard matches directories named foo/__/bar with string slug', function() { 408 | processRequest('/wildcard-extended/abc/foobar', 'GET'); 409 | 410 | assert.equal(res.status, 200); 411 | assert.equal(res.body, 'wildcards-extended'); 412 | }); 413 | 414 | it('wildcard matches directories named foo/__/bar/__/fizz', function() { 415 | processRequest('/wildcard-extended/abc/foobar/def/fizzbuzz', 'GET'); 416 | 417 | assert.equal(res.status, 200); 418 | assert.equal(res.body, 'wildcards-extended-multiple'); 419 | }); 420 | 421 | it('__ not used if more specific match exist', function() { 422 | processRequest('/wildcard/exact', 'GET'); 423 | 424 | assert.equal(res.status, 200); 425 | assert.equal(res.body, 'more specific\n'); 426 | }); 427 | 428 | it('should not resolve with missing slug', function() { 429 | processRequest('/wildcard/', 'GET'); 430 | 431 | assert.equal(res.status, 404); 432 | }); 433 | }); 434 | describe('.getResponseDelay', function() { 435 | it('should return a value greater than zero when valid', function() { 436 | const ownValueHeaders = [ 437 | { 'Response-Delay': 1 }, 438 | { 'Response-Delay': 99 }, 439 | { 'Response-Delay': '9999' }, 440 | ]; 441 | ownValueHeaders.forEach(function(header) { 442 | const val = mockserver.getResponseDelay(header); 443 | assert(val > 0, `Value found was ${val}`); 444 | }); 445 | }); 446 | it('should return zero for invalid values', function() { 447 | const zeroValueHeaders = [ 448 | { 'Response-Delay': 'a' }, 449 | { 'Response-Delay': '' }, 450 | { 'Response-Delay': '-1' }, 451 | { 'Response-Delay': -1 }, 452 | { 'Response-Delay': 0 }, 453 | {}, 454 | null, 455 | undefined, 456 | ]; 457 | zeroValueHeaders.forEach(function(header) { 458 | const val = mockserver.getResponseDelay(header); 459 | assert.equal(val, 0, `Value found was ${val}`); 460 | }); 461 | }); 462 | }); 463 | describe('Custom response codes', function() { 464 | it('response status codes depends on request case 400 Bad request', function (done) { 465 | var req = new MockReq({ 466 | method: 'POST', 467 | url: '/headerimportjs', 468 | headers: {}, 469 | }); 470 | req.write(JSON.stringify({ baz: '123' })); 471 | req.end(); 472 | 473 | mockserver(mocksDirectory, verbose)(req, res); 474 | 475 | req.on('end', function () { 476 | assert.equal(res.status, '400'); 477 | done(); 478 | }); 479 | }); 480 | it('response status codes depends on request case 200 OK', function (done) { 481 | var req = new MockReq({ 482 | method: 'POST', 483 | url: '/headerimportjs', 484 | headers: {}, 485 | }); 486 | req.write(JSON.stringify({ foo: '123' })); 487 | req.end(); 488 | 489 | mockserver(mocksDirectory, verbose)(req, res); 490 | 491 | req.on('end', function () { 492 | assert.equal(res.status, '200'); 493 | done(); 494 | }); 495 | }); 496 | }) 497 | }); 498 | }); 499 | 500 | describe('Monad methods', function() { 501 | let m; 502 | function fn(val) { 503 | return { 504 | ...val, 505 | b: 2 506 | }; 507 | } 508 | const testData = { a: 1 }; 509 | const expectData = { a: 1, b: 2 }; 510 | beforeEach(function() { 511 | m = Monad.of(testData); 512 | }); 513 | 514 | it('Monad have static method `of`', function() { 515 | assert.equal(typeof Monad.of, 'function'); 516 | }); 517 | it('Monad method `of` should return Object type Monad', function() { 518 | assert.equal(m instanceof Monad, true); 519 | }); 520 | it('Monad method `map` should recive value and return Object type Monad', function() { 521 | assert.equal(m.map(fn) instanceof Monad, true); 522 | }); 523 | it('Monad method `join` should return value', function () { 524 | assert.strictEqual(m.join(), testData); 525 | }); 526 | it('Monad method `chain` should return value', function () { 527 | assert.deepEqual(m.chain(fn), expectData); 528 | }); 529 | }); 530 | --------------------------------------------------------------------------------