├── .circleci └── config.yml ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .jsdoc ├── .mdlrc ├── .npmrc ├── .scannerwork └── .sonar_lock ├── .snyk ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── mb ├── firebase.json ├── images ├── Intellij-Configuration-Details.png ├── Intellij-Configurations.png └── sources │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── README │ └── sources.pptx ├── mbTest ├── api.js ├── api │ ├── config.json │ ├── homeControllerTest.js │ ├── http │ │ ├── httpBehaviorsTest.js │ │ ├── httpFaultTest.js │ │ ├── httpImposterTest.js │ │ ├── httpInjectionTest.js │ │ ├── httpMetricsTest.js │ │ ├── httpProxyStubTest.js │ │ └── httpStubTest.js │ ├── https │ │ ├── cert │ │ │ ├── cert.pem │ │ │ ├── csr.pem │ │ │ └── key.pem │ │ └── httpsCertificateTest.js │ ├── impostersControllerTest.js │ ├── smtp │ │ ├── smtpClient.js │ │ └── smtpImposterTest.js │ └── tcp │ │ ├── tcpBehaviorsTest.js │ │ ├── tcpClient.js │ │ ├── tcpImposterTest.js │ │ ├── tcpInjectionTest.js │ │ ├── tcpProxyTest.js │ │ └── tcpStubTest.js ├── baseHttpClient.js ├── cli │ ├── config.json │ ├── configFileTest.js │ ├── dataStringify │ │ ├── imposters.ejs │ │ └── services │ │ │ ├── body.ejs │ │ │ └── data.ejs │ ├── datadirTest.js │ ├── debugTest.js │ ├── formatterTest.js │ ├── formatters │ │ ├── asyncBase64Formatter.js │ │ └── base64Formatter.js │ ├── gzip.json │ ├── hostTest.js │ ├── imposters │ │ ├── account.xml │ │ ├── accounts.json │ │ ├── email.json │ │ ├── imposters.ejs │ │ ├── orders.json │ │ ├── users.json │ │ └── users.xml │ ├── nestedStringify │ │ ├── imposters.ejs │ │ └── services │ │ │ ├── body.ejs │ │ │ ├── data.ejs │ │ │ └── response.ejs │ ├── noparse.json │ ├── rcfileTest.js │ ├── replayTest.js │ ├── saveTest.js │ ├── securityTest.js │ └── templates │ │ ├── counter.ejs │ │ ├── imposters.ejs │ │ ├── originServer.ejs │ │ ├── originServerResponse.ejs │ │ ├── proxy.ejs │ │ └── proxyServer.ejs ├── js │ ├── config.json │ └── createAppTest.js ├── mb.js ├── package-lock.json ├── package.json ├── perf │ ├── config.json │ ├── databaseConcurrencyTest.js │ ├── loadTest.js │ └── scenarios │ │ ├── baseline.json │ │ └── singleStub.json └── web │ ├── config.json │ ├── contractTest.js │ ├── crawler.js │ ├── crawlerTest.js │ ├── docsIntegrityTest.js │ ├── docsTester │ ├── docs.js │ ├── docsTestScenario.js │ └── testTypes │ │ ├── exec.js │ │ ├── file.js │ │ ├── http.js │ │ └── smtp.js │ ├── feedTest.js │ └── validatorTest.js ├── package-lock.json ├── package.json ├── releases.json ├── scripts ├── codeclimate ├── printVersion ├── publishDocker ├── publishHeroku ├── publishNpm └── sonar ├── src ├── cli │ ├── api.js │ └── cli.js ├── controllers │ ├── configController.js │ ├── feedController.js │ ├── homeController.js │ ├── imposterController.js │ ├── impostersController.js │ └── logsController.js ├── models │ ├── behaviors.js │ ├── behaviorsValidator.js │ ├── compatibility.js │ ├── dryRunValidator.js │ ├── filesystemBackedImpostersRepository.js │ ├── http │ │ ├── baseHttpServer.js │ │ ├── headersMap.js │ │ ├── httpProxy.js │ │ ├── httpRequest.js │ │ ├── httpServer.js │ │ └── index.js │ ├── https │ │ ├── cert │ │ │ ├── mb-cert.pem │ │ │ ├── mb-csr.pem │ │ │ └── mb-key.pem │ │ ├── httpsServer.js │ │ └── index.js │ ├── imposter.js │ ├── imposterPrinter.js │ ├── impostersRepository.js │ ├── inMemoryImpostersRepository.js │ ├── jsonpath.js │ ├── mbConnection.js │ ├── predicates.js │ ├── protocols.js │ ├── responseResolver.js │ ├── smtp │ │ ├── index.js │ │ ├── smtpRequest.js │ │ └── smtpServer.js │ ├── tcp │ │ ├── index.js │ │ ├── tcpProxy.js │ │ ├── tcpRequest.js │ │ ├── tcpServer.js │ │ └── tcpValidator.js │ └── xpath.js ├── mountebank.js ├── public │ ├── images │ │ ├── arrow_down.png │ │ ├── arrow_up.png │ │ ├── book.jpg │ │ ├── dataflow.png │ │ ├── favicon.ico │ │ ├── forkme_right_orange_ff7600.png │ │ ├── mountebank.png │ │ ├── overview.gif │ │ ├── quote.png │ │ └── tw-logo.png │ ├── scripts │ │ ├── jquery │ │ │ └── jquery-3.6.1.min.js │ │ └── urlHashHandler.js │ └── stylesheets │ │ ├── application.css │ │ ├── ie.css │ │ ├── imposters.css │ │ └── jqueryui │ │ └── 1.10.4 │ │ └── themes │ │ └── smoothness │ │ └── jquery-ui.css ├── util │ ├── combinators.js │ ├── date.js │ ├── errors.js │ ├── helpers.js │ ├── inherit.js │ ├── ip.js │ ├── logger.js │ ├── middleware.js │ └── scopedLogger.js └── views │ ├── _footer.ejs │ ├── _header.ejs │ ├── _imposter.ejs │ ├── config.ejs │ ├── docs │ ├── api │ │ ├── behaviors.ejs │ │ ├── behaviors │ │ │ ├── copy.ejs │ │ │ ├── decorate.ejs │ │ │ ├── lookup.ejs │ │ │ ├── shellTransform.ejs │ │ │ └── wait.ejs │ │ ├── contracts.ejs │ │ ├── contracts │ │ │ ├── addStub-description.ejs │ │ │ ├── addStub.ejs │ │ │ ├── config-description.ejs │ │ │ ├── config.ejs │ │ │ ├── home-description.ejs │ │ │ ├── home.ejs │ │ │ ├── imposter-description.ejs │ │ │ ├── imposter.ejs │ │ │ ├── imposters-description.ejs │ │ │ ├── imposters.ejs │ │ │ ├── logs-description.ejs │ │ │ ├── logs.ejs │ │ │ ├── stub-description.ejs │ │ │ ├── stub.ejs │ │ │ ├── stubs-description.ejs │ │ │ └── stubs.ejs │ │ ├── errors.ejs │ │ ├── fault │ │ │ ├── connectionReset.ejs │ │ │ └── randomDataThenClose.ejs │ │ ├── faults.ejs │ │ ├── injection.ejs │ │ ├── json.ejs │ │ ├── jsonpath.ejs │ │ ├── mocks.ejs │ │ ├── overview.ejs │ │ ├── predicates.ejs │ │ ├── predicates │ │ │ ├── and.ejs │ │ │ ├── contains.ejs │ │ │ ├── deepEquals.ejs │ │ │ ├── endsWith.ejs │ │ │ ├── equals.ejs │ │ │ ├── exists.ejs │ │ │ ├── inject.ejs │ │ │ ├── matches.ejs │ │ │ ├── not.ejs │ │ │ ├── or.ejs │ │ │ └── startsWith.ejs │ │ ├── proxies.ejs │ │ ├── proxy │ │ │ ├── addDecorateBehavior.ejs │ │ │ ├── addWaitBehavior.ejs │ │ │ ├── injectHeaders.ejs │ │ │ ├── predicateGenerators.ejs │ │ │ └── proxyModes.ejs │ │ ├── stubs.ejs │ │ └── xpath.ejs │ ├── cli │ │ ├── configFiles.ejs │ │ ├── customFormatters.ejs │ │ ├── help.ejs │ │ ├── replay.ejs │ │ ├── restart.ejs │ │ ├── save.ejs │ │ ├── start.ejs │ │ └── stop.ejs │ ├── commandLine.ejs │ ├── communityExtensions.ejs │ ├── gettingStarted.ejs │ ├── mentalModel.ejs │ ├── protocols │ │ ├── custom.ejs │ │ ├── http.ejs │ │ ├── https.ejs │ │ ├── smtp.ejs │ │ └── tcp.ejs │ └── security.ejs │ ├── faqs.ejs │ ├── feed.ejs │ ├── imposter.ejs │ ├── imposters.ejs │ ├── index.ejs │ ├── license.ejs │ ├── logs.ejs │ ├── releases.ejs │ ├── releases │ ├── v1.1.0.ejs │ ├── v1.1.36.ejs │ ├── v1.1.72.ejs │ ├── v1.10.0.ejs │ ├── v1.11.0.ejs │ ├── v1.12.0.ejs │ ├── v1.13.0.ejs │ ├── v1.14.0.ejs │ ├── v1.14.1.ejs │ ├── v1.15.0.ejs │ ├── v1.16.0.ejs │ ├── v1.2.0.ejs │ ├── v1.2.103.ejs │ ├── v1.2.122.ejs │ ├── v1.2.30.ejs │ ├── v1.2.45.ejs │ ├── v1.2.56.ejs │ ├── v1.3.0.ejs │ ├── v1.3.1.ejs │ ├── v1.4.0.ejs │ ├── v1.4.1.ejs │ ├── v1.4.2.ejs │ ├── v1.4.3.ejs │ ├── v1.5.0.ejs │ ├── v1.5.1.ejs │ ├── v1.6.0.ejs │ ├── v1.7.0.ejs │ ├── v1.7.1.ejs │ ├── v1.7.2.ejs │ ├── v1.8.0.ejs │ ├── v1.9.0.ejs │ ├── v2.0.0.ejs │ ├── v2.1.0.ejs │ ├── v2.1.1.ejs │ ├── v2.1.2.ejs │ ├── v2.2.0.ejs │ ├── v2.2.1.ejs │ ├── v2.3.0.ejs │ ├── v2.3.1.ejs │ ├── v2.3.2.ejs │ ├── v2.3.3.ejs │ ├── v2.4.0.ejs │ ├── v2.5.0.ejs │ ├── v2.6.0.ejs │ ├── v2.7.0.ejs │ ├── v2.8.0.ejs │ ├── v2.8.1.ejs │ ├── v2.8.2.ejs │ ├── v2.9.0.ejs │ └── v2.9.1.ejs │ ├── sitemap.ejs │ └── support.ejs ├── tasks ├── createProtocolsFile.js ├── deploy │ └── docs.js ├── dist.js ├── lints │ ├── deadCheck.js │ ├── licenseCheck.js │ ├── objectCheck.js │ └── shared │ │ └── scan.js ├── mb.js ├── mbtest.js ├── run.js └── version.js └── test ├── config.json ├── controllers ├── feedControllerTest.js ├── homeControllerTest.js ├── imposterControllerTest.js ├── impostersControllerTest.js └── logsControllerTest.js ├── fakes ├── fakeImpostersRepository.js ├── fakeLogger.js ├── fakeRequest.js └── fakeResponse.js ├── mock.js ├── models ├── behaviors │ ├── copyTest.js │ ├── decorateTest.js │ ├── lookupTest.js │ ├── shellTransformTest.js │ └── waitTest.js ├── compatibilityTest.js ├── dryRunValidatorTest.js ├── filesystemBackedImpostersRepositoryTest.js ├── http │ ├── headersHelperTest.js │ └── httpRequestTest.js ├── imposterTest.js ├── impostersRepositoryContractTest.js ├── impostersRepositoryTest.js ├── predicates │ ├── andTest.js │ ├── containsTest.js │ ├── deepEqualsTest.js │ ├── endsWithTest.js │ ├── equalsTest.js │ ├── existsTest.js │ ├── injectTest.js │ ├── jsonTest.js │ ├── jsonpathTest.js │ ├── matchesTest.js │ ├── notTest.js │ ├── orTest.js │ ├── startsWithTest.js │ └── xpathTest.js ├── protocolsTest.js ├── responseResolverTest.js ├── smtp │ └── smtpRequestTest.js └── tcp │ ├── tcpRequestTest.js │ └── tcpValidatorTest.js ├── public └── scripts │ └── urlHashHandlerTest.js └── util ├── combinatorsTest.js ├── dateTest.js ├── errorsTest.js ├── helpersTest.js ├── inheritTest.js ├── ipTest.js ├── middlewareTest.js └── scopedLoggerTest.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | plugins: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - javascript 10 | eslint: 11 | enabled: true 12 | fixme: 13 | enabled: true 14 | markdownlint: 15 | enabled: true 16 | nodesecurity: 17 | enabled: true 18 | shellcheck: 19 | enabled: true 20 | exclude_patterns: 21 | - "**/node_modules/" 22 | - "dist" 23 | - "test" 24 | - "mbTest" 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{yml,json}] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/public/scripts/jquery/**/*.js 2 | mbTest/perf/loadTest.js 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | # *.c text 7 | 8 | # Declare files that will always have CRLF line endings on checkout. 9 | # *.sln text eol=crlf 10 | 11 | # Denote all files that are truly binary and should not be modified. 12 | *.png binary 13 | *.jpg binary 14 | *.gif binary 15 | *.ico binary 16 | *.pptx binary 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ 3 | .DS_Store 4 | mb*.log 5 | *.pid 6 | *.swp 7 | *.swo 8 | /dist/ 9 | !scripts/dist/ 10 | mountebank.iml 11 | TODO 12 | request.txt 13 | /docs 14 | /coverage 15 | troubleshooting/ 16 | protocols.json 17 | metrics/ 18 | .idea-bk 19 | .nyc_output/ 20 | cc-test-reporter 21 | testResults 22 | tmp/ 23 | .scannerwork/ 24 | -------------------------------------------------------------------------------- /.jsdoc: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "plugins": ["plugins/markdown"], 6 | "templates": { 7 | "cleverLinks": false, 8 | "monospaceLinks": false, 9 | "outputSourceFiles": false, 10 | "outputSourcePath": false 11 | }, 12 | "markdown": { 13 | "parser": "gfm", 14 | "hardwrap": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | # Used by markdownlint in codeclimate 2 | # Disable rules 3 | rules '~MD013', # Line Length 4 | '~MD026', # Trailing punctuation in header 5 | '~MD032' # Lists should be surrounded by blank line (includes long bullet points broken into multiple lines) 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact:true 2 | -------------------------------------------------------------------------------- /.scannerwork/.sonar_lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/.scannerwork/.sonar_lock -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-450202: 7 | - mailparser > html-to-text > lodash: 8 | patched: '2019-07-03T22:06:43.790Z' 9 | - winston > async > lodash: 10 | patched: '2019-07-03T22:06:43.790Z' 11 | - snyk > lodash: 12 | patched: '2019-08-17T08:01:25.812Z' 13 | - snyk > inquirer > lodash: 14 | patched: '2019-08-17T08:01:25.812Z' 15 | - snyk > snyk-config > lodash: 16 | patched: '2019-08-17T08:01:25.812Z' 17 | - snyk > snyk-nodejs-lockfile-parser > lodash: 18 | patched: '2019-08-17T08:01:25.812Z' 19 | 'npm:extend:20180424': 20 | - snyk > proxy-agent > pac-proxy-agent > get-uri > extend: 21 | patched: '2019-08-17T08:01:25.812Z' 22 | SNYK-JS-HTTPSPROXYAGENT-469131: 23 | - https-proxy-agent: 24 | patched: '2019-10-03T22:06:49.004Z' 25 | - snyk > proxy-agent > https-proxy-agent: 26 | patched: '2019-10-03T22:06:49.004Z' 27 | - snyk > proxy-agent > pac-proxy-agent > https-proxy-agent: 28 | patched: '2019-10-03T22:06:49.004Z' 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM node:18-alpine 4 | 5 | WORKDIR /app 6 | 7 | # Install tarball to allow the command to be 'mb' instead of 'bin/mb' 8 | COPY mountebank-*.tgz ./ 9 | RUN npm install --production -g mountebank-*.tgz && npm cache clean -f 10 | 11 | # Run as a non-root user 12 | RUN adduser -D mountebank 13 | RUN chown -R mountebank /app 14 | USER mountebank 15 | 16 | EXPOSE 2525 17 | 18 | ENTRYPOINT ["mb"] 19 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | #### Expected behaviour 8 | 9 | ... 10 | 11 | #### Actual behaviour 12 | 13 | ... 14 | 15 | #### Steps to reproduce 16 | 17 | ... 18 | 19 | #### Software versions used 20 | 21 | ``` 22 | OS : 23 | mountebank : 24 | node.js : 25 | (only if installed via npm) 26 | Installation method : 27 | (npm, zip, tar, pkg, deb, rpm) 28 | ``` 29 | 30 | #### Log contents in mb.log when running mb --loglevel debug 31 | 32 | 35 | 36 | ``` 37 | Log contents here 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 mountebank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for mountebank. 4 | 5 | * [Reporting a Bug](#reporting-a-bug) 6 | * [Disclosure Policy](#disclosure-policy) 7 | * [Comments on this Policy](#comments-on-this-policy) 8 | 9 | ## Reporting a Bug 10 | 11 | Thank you for improving the security of mountebank. I sincerely appreciate your efforts 12 | and responsible disclosure and will make every effort to acknowledge your 13 | contributions. 14 | 15 | Please report security bugs by emailing me directly at brandon.byars@gmail.com. 16 | 17 | Following your report, I will investigate and keep you informed of the progress towards 18 | a fix and full announcement. Along the way, I may ask for additional information or 19 | guidance. 20 | 21 | Report security bugs in third-party modules to the person or team maintaining 22 | the module. You can also report a vulnerability through the 23 | [Node Security Project](https://nodesecurity.io/report). 24 | 25 | ## Disclosure Policy 26 | 27 | In most cases, I will include a full disclosure in the release notes for the release 28 | that fixes the security bug. 29 | 30 | ## Comments on this Policy 31 | 32 | If you have suggestions on how this process could be improved please submit a 33 | pull request. 34 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": ".", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /images/Intellij-Configuration-Details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/Intellij-Configuration-Details.png -------------------------------------------------------------------------------- /images/Intellij-Configurations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/Intellij-Configurations.png -------------------------------------------------------------------------------- /images/sources/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/1.png -------------------------------------------------------------------------------- /images/sources/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/10.png -------------------------------------------------------------------------------- /images/sources/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/11.png -------------------------------------------------------------------------------- /images/sources/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/12.png -------------------------------------------------------------------------------- /images/sources/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/13.png -------------------------------------------------------------------------------- /images/sources/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/14.png -------------------------------------------------------------------------------- /images/sources/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/2.png -------------------------------------------------------------------------------- /images/sources/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/3.png -------------------------------------------------------------------------------- /images/sources/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/4.png -------------------------------------------------------------------------------- /images/sources/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/5.png -------------------------------------------------------------------------------- /images/sources/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/6.png -------------------------------------------------------------------------------- /images/sources/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/7.png -------------------------------------------------------------------------------- /images/sources/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/8.png -------------------------------------------------------------------------------- /images/sources/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/9.png -------------------------------------------------------------------------------- /images/sources/README: -------------------------------------------------------------------------------- 1 | Created with http://gifmaker.me/ 2 | Used 400x173 px canvas 3 | Set 800 ms as animation speed 4 | 5 | -------------------------------------------------------------------------------- /images/sources/sources.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/images/sources/sources.pptx -------------------------------------------------------------------------------- /mbTest/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const httpClient = require('./baseHttpClient').create('http'), 4 | assert = require('assert'), 5 | headers = { connection: 'close' }; // prevent hanging tests with connections left open 6 | 7 | function create (port) { 8 | port = port || parseInt(process.env.MB_PORT || 2525); 9 | 10 | function get (path) { 11 | return httpClient.get(path, port, headers); 12 | } 13 | 14 | function post (path, body) { 15 | return httpClient.post(path, body, port, headers); 16 | } 17 | 18 | function del (path) { 19 | return httpClient.del(path, port, headers); 20 | } 21 | 22 | function put (path, body) { 23 | return httpClient.put(path, body, port, headers); 24 | } 25 | 26 | async function createImposter (imposter) { 27 | const response = await post('/imposters', imposter); 28 | assert.strictEqual(response.statusCode, 201, JSON.stringify(response.body, null, 2)); 29 | return response; 30 | } 31 | 32 | return { 33 | url: `http://localhost:${port}`, 34 | port, 35 | get, post, del, put, 36 | createImposter 37 | }; 38 | } 39 | 40 | module.exports = { create }; 41 | -------------------------------------------------------------------------------- /mbTest/api/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "xunit, spec", 3 | "xunitReporterOptions": { 4 | "output": "testResults/api/results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mbTest/api/homeControllerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | api = require('../api').create(); 5 | 6 | describe('GET /', function () { 7 | it('should return correct hypermedia', async function () { 8 | const homeResponse = await api.get('/'); 9 | assert.strictEqual(homeResponse.statusCode, 200); 10 | 11 | const links = homeResponse.body._links, 12 | impostersResponse = await api.get(links.imposters.href); 13 | assert.strictEqual(impostersResponse.statusCode, 200); 14 | 15 | const configResponse = await api.get(links.config.href); 16 | assert.strictEqual(configResponse.statusCode, 200); 17 | 18 | const logsResponse = await api.get(links.logs.href); 19 | assert.strictEqual(logsResponse.statusCode, 200); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /mbTest/api/http/httpFaultTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | api = require('../../api').create(), 5 | BaseHttpClient = require('../../baseHttpClient'), 6 | port = api.port + 1, 7 | timeout = parseInt(process.env.MB_SLOW_TEST_TIMEOUT || 2000); 8 | 9 | ['http', 'https'].forEach(protocol => { 10 | const client = BaseHttpClient.create(protocol); 11 | 12 | describe(`${protocol} imposter`, function () { 13 | this.timeout(timeout); 14 | 15 | afterEach(async function () { 16 | await api.del('/imposters'); 17 | }); 18 | 19 | describe('POST /imposters with stubs', function () { 20 | it('should drop the connection when fault CONNECTION_RESET_BY_PEER is specified', async function () { 21 | const stub = { responses: [{ fault: 'CONNECTION_RESET_BY_PEER' }] }, 22 | request = { protocol, port, stubs: [stub] }; 23 | await api.createImposter(request); 24 | 25 | try { 26 | await client.get('/', port); 27 | assert.fail('did not close socket'); 28 | } 29 | catch (error) { 30 | assert.strictEqual(error.code, 'ECONNRESET'); 31 | } 32 | }); 33 | 34 | it('should write garbage then drop the connection when fault RANDOM_DATA_THEN_CLOSE is specified', async function () { 35 | const stub = { responses: [{ fault: 'RANDOM_DATA_THEN_CLOSE' }] }, 36 | request = { protocol, port, stubs: [stub] }; 37 | await api.createImposter(request); 38 | 39 | try { 40 | await client.get('/', port); 41 | assert.fail('did not close socket'); 42 | } 43 | catch (error) { 44 | assert.strictEqual(error.code, 'HPE_INVALID_CONSTANT'); 45 | } 46 | }); 47 | 48 | it('should do nothing when undefined fault is specified', async function () { 49 | const stub = { responses: [{ fault: 'NON_EXISTENT_FAULT' }] }, 50 | request = { protocol, port, stubs: [stub] }; 51 | await api.createImposter(request); 52 | const response = await client.get('/', port); 53 | assert.strictEqual(response.statusCode, 200); 54 | assert.strictEqual(response.body, ''); 55 | }); 56 | 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /mbTest/api/https/cert/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDejCCAmICCQDlIe97PDjXJDANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJV 3 | UzEOMAwGA1UECBMFVGV4YXMxFTATBgNVBAoTDFRob3VnaHRXb3JrczEMMAoGA1UE 4 | CxMDT1NTMRMwEQYDVQQDEwptYnRlc3Qub3JnMSYwJAYJKoZIhvcNAQkBFhdicmFu 5 | ZG9uLmJ5YXJzQGdtYWlsLmNvbTAeFw0xNTA1MDMyMDE3NTRaFw0xNTA2MDIyMDE3 6 | NTRaMH8xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIEwVUZXhhczEVMBMGA1UEChMMVGhv 7 | dWdodFdvcmtzMQwwCgYDVQQLEwNPU1MxEzARBgNVBAMTCm1idGVzdC5vcmcxJjAk 8 | BgkqhkiG9w0BCQEWF2JyYW5kb24uYnlhcnNAZ21haWwuY29tMIIBIjANBgkqhkiG 9 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5V88ZyZ5hkPF7MzaDMvhGtGSBKIhQia2a0vW 10 | 6VfEtf/Dk80qKaalrwiBZlXheT/zwCoO7WBeqh5agOs0CSwzzEEie5/J6yVfgEJb 11 | VROpnMbrLSgnUJXRfGNf0LCnTymGMhufz2utzcHRtgLm3nf5zQbBJ8XkOaPXokuE 12 | UWwmTHrqeTN6munoxtt99olzusraxpgiGCil2ppFctsQHle49Vjs88KuyVjC5AOb 13 | +P7Gqwru+R/1vBLyD8NVNl1WhLqaaeaopb9CcPgFZClchuMaAD4cecndrt5w4iuL 14 | q91g71AjdXSG6V3R0DC2Yp/ud0Z8wXsMMC6X6VUxFrbeajo8CQIDAQABMA0GCSqG 15 | SIb3DQEBBQUAA4IBAQCobQRpj0LjEcIViG8sXauwhRhgmmEyCDh57psWaZ2vdLmM 16 | ED3D6y3HUzz08yZkRRr32VEtYhLldc7CHItscD+pZGJWlpgGKXEHdz/EqwR8yVhi 17 | akBMhHxSX9s8N8ejLyIOJ9ToJQOPgelI019pvU4cmiDLihK5tezCrZfWNHXKw1hw 18 | Sh/nGJ1UddEHCtC78dz6uIVIJQC0PkrLeGLKyAFrFJp4Bim8W8fbYSAffsWNATC+ 19 | dVKUlunVLd4RX/73nY5EM3ErcDDOCdUEQ2fUT59FhQF89DihFG4xW4OLq42/pgmW 20 | KQBvwwfJxIFqg4fdnJUkHoLX3+glQWWrz80cauVH 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /mbTest/api/https/cert/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICxDCCAawCAQAwfzELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRUwEwYD 3 | VQQKEwxUaG91Z2h0V29ya3MxDDAKBgNVBAsTA09TUzETMBEGA1UEAxMKbWJ0ZXN0 4 | Lm9yZzEmMCQGCSqGSIb3DQEJARYXYnJhbmRvbi5ieWFyc0BnbWFpbC5jb20wggEi 5 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlXzxnJnmGQ8XszNoMy+Ea0ZIE 6 | oiFCJrZrS9bpV8S1/8OTzSoppqWvCIFmVeF5P/PAKg7tYF6qHlqA6zQJLDPMQSJ7 7 | n8nrJV+AQltVE6mcxustKCdQldF8Y1/QsKdPKYYyG5/Pa63NwdG2Aubed/nNBsEn 8 | xeQ5o9eiS4RRbCZMeup5M3qa6ejG2332iXO6ytrGmCIYKKXamkVy2xAeV7j1WOzz 9 | wq7JWMLkA5v4/sarCu75H/W8EvIPw1U2XVaEuppp5qilv0Jw+AVkKVyG4xoAPhx5 10 | yd2u3nDiK4ur3WDvUCN1dIbpXdHQMLZin+53RnzBewwwLpfpVTEWtt5qOjwJAgMB 11 | AAGgADANBgkqhkiG9w0BAQsFAAOCAQEAvPIi+SIKuGptuAsh4EYf26VXHv7dvAtu 12 | xeB8CJo6EdnEiBwewUoJbaoCM2bt2fqQ3XmZFBGbzadVgV/YVs7xj89ApT53xRtu 13 | 8hsKaJTf618efpuRjRnjNXSbotwe8kCflSF1sNmZ7k6lDJvGsYTWIvtgKq5H3PNU 14 | UQx5DlmwRdyjL7VuSt2QMDvhLo0fG3LdVIRkmluszuzddohspqhvzD0UAcpKXTLa 15 | RZZuK3TNa8Bx2d+cencsXNjj/6GvUD5jvhCcxLmDOb4A/4i6PDQr/wX/KnuPdCRh 16 | XMamWBIzjGraXMerVPSZMEaRzG3AJDeG8+gvCuYSg7jWDyz0dlFHoA== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /mbTest/api/https/cert/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA5V88ZyZ5hkPF7MzaDMvhGtGSBKIhQia2a0vW6VfEtf/Dk80q 3 | KaalrwiBZlXheT/zwCoO7WBeqh5agOs0CSwzzEEie5/J6yVfgEJbVROpnMbrLSgn 4 | UJXRfGNf0LCnTymGMhufz2utzcHRtgLm3nf5zQbBJ8XkOaPXokuEUWwmTHrqeTN6 5 | munoxtt99olzusraxpgiGCil2ppFctsQHle49Vjs88KuyVjC5AOb+P7Gqwru+R/1 6 | vBLyD8NVNl1WhLqaaeaopb9CcPgFZClchuMaAD4cecndrt5w4iuLq91g71AjdXSG 7 | 6V3R0DC2Yp/ud0Z8wXsMMC6X6VUxFrbeajo8CQIDAQABAoIBAQDR7TjuO0NsA+lJ 8 | Ei/bGFn83qOig/Smg6HfT7jxNXf/DoekZVjaaAAp00IDNES5YTOow5WH4fSiBEfW 9 | x+2HG6KIpXB27sax5TGjxJespONObqxdVuuskIH7M0RFTvjBtgJxJ+E6yJV4xJQ3 10 | 3i4InIvO/wiXa0G3E2JO0ojWsIHY6cZZ6XPCnmH2go1Hkpf25ABO2ZAwRo92YvXT 11 | nWMgtZJeQifDd3vOtf3oFr349cTW8frtO9afO6zSPo7ePDePYjsXBdYlAKt6iw5c 12 | ePwPxiOcPMFcFg+JHiOIDWu8Xf4/FAse6I7uZg9XPnERr50JdMyjp3ab8YkRcUVe 13 | stD8W0E1AoGBAP22Z0U6yXlvu856UT5YFoD+At51vmk9qYB9AXPd3j0w4/99ZXZo 14 | W9Ij3kSc3INpNOQF5lr1Uwvi0WPDmRBPGt8mVKt6rsNLEey+cvqD/I4nOwnj4ahf 15 | kXsOndcJy6/EchTyiowwH+ecLef2oQqj0ewqg9ONkmgSft0+RNOKH+TDAoGBAOdw 16 | puhMjqZVjiJYebdPfXIyfX7VIO9G22cbww4TQZSkY46BPO1fM1rjJj3ViwFIirvw 17 | i7NWqEnIbWhsqb7AuDThSnpCn+ve4Yen/XXMhEGmn0m96eZlykrHzLPTzASwIGKW 18 | V+cjShI/qeBnCIc/hJqGEoxYq5tvoGXMAfc2P19DAoGBALXsYjaZBbjm7qMKwFDU 19 | 9yX9fe8oPKXYCj/Q4wbDM8Vq1kHwF54FDLos8AcA93nfKYbL2DamrKrcNEq2CX5R 20 | SrJyGMpxCPmOTccVdPq/Q4xnAwV/euKxzG6Rt4pnQr/BPBBbLg+Bvo7kRWKMTUOE 21 | 3lTa03YRkMbBZGieatnItD/jAoGALi1X3/mGbMGdPlUQ0/ZUPTi9uVSk4ZjtpcDw 22 | RhqpdxLlmkVG9sWL5ZJ0ytmUhokGxUDVXs7nfPE0gyVLKPgiyMo6pfItHk62CO8H 23 | rz1DFiY8meX+iS2+EvWSbj7P8g+CJqmAmGl+Ge4B4vgrx3Bw8LPXd5EpjDXVdvoh 24 | WkXElykCgYBmNq3GXmQfLVsTLg1zp5T6Fnj18AHkci2BVeCNlcQMhbUp7gb9QYFV 25 | 7gQrbjGpZ+Nr7ImSb0KgMlL63ZajtDknEuuJHAzmfQfMyPiLPQLGIGQoYXrxRmMb 26 | 3yPokemcjP2Xk60I3fjPDydOaSL/QWXA8aXn/K9hbvDs7nblA/eq4g== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /mbTest/api/smtp/smtpClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SMTPConnection = require('nodemailer/lib/smtp-connection'); 4 | 5 | function addressOf (email) { 6 | if (email.indexOf('<') < 0) { 7 | return email; 8 | } 9 | return (/<([^>]+)>/).exec(email)[1]; 10 | } 11 | 12 | function messageText (message) { 13 | let result = `From: ${message.from}`; 14 | message.to.forEach(address => { result += `\r\nTo: ${address}`; }); 15 | message.cc.forEach(address => { result += `\r\nCc: ${address}`; }); 16 | message.bcc.forEach(address => { result += `\r\nBcc: ${address}`; }); 17 | result += `\r\nSubject: ${message.subject}`; 18 | result += `\r\n\r\n${message.text}`; 19 | return result; 20 | } 21 | 22 | function send (message, port, host = '127.0.0.1') { 23 | if (!port) { 24 | throw Error('you forgot to pass the port again'); 25 | } 26 | 27 | message.cc = message.cc || []; 28 | message.bcc = message.bcc || []; 29 | 30 | return new Promise((resolve, reject) => { 31 | const connection = new SMTPConnection({ port, host }); 32 | 33 | connection.on('error', reject); 34 | 35 | connection.connect(connectionError => { 36 | if (connectionError) { 37 | reject(connectionError); 38 | } 39 | let envelope = { 40 | from: message.envelopeFrom || addressOf(message.from), 41 | to: message.envelopeTo || message.to.concat(message.cc).concat(message.bcc).map(addressOf) 42 | }; 43 | connection.send(envelope, messageText(message), (sendError, info) => { 44 | if (sendError) { 45 | reject(sendError); 46 | } 47 | connection.quit(); 48 | resolve(info); 49 | }); 50 | }); 51 | }); 52 | } 53 | 54 | module.exports = { 55 | send: send 56 | }; 57 | -------------------------------------------------------------------------------- /mbTest/api/tcp/tcpClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | 5 | function send (message, serverPort, timeout, serverHost, expectedLength = 0) { 6 | const options = { port: serverPort }; 7 | 8 | if (serverHost) { 9 | options.host = serverHost; 10 | } 11 | 12 | return new Promise((resolve, reject) => { 13 | const socket = net.createConnection(options, () => { socket.write(message); }), 14 | packets = []; 15 | 16 | if (!serverPort) { 17 | throw Error('you forgot to pass the port again'); 18 | } 19 | 20 | socket.once('error', reject); 21 | socket.on('data', data => { 22 | packets.push(data); 23 | const payload = Buffer.concat(packets); 24 | if (payload.length >= expectedLength) { 25 | resolve(payload); 26 | } 27 | }); 28 | 29 | if (timeout) { 30 | setTimeout(() => { resolve(''); }, timeout); 31 | } 32 | }); 33 | } 34 | 35 | function fireAndForget (message, serverPort) { 36 | return new Promise((resolve, reject) => { 37 | const socket = net.createConnection({ port: serverPort }, () => { socket.write(message); }); 38 | 39 | // Attempt to avoid race conditions where the subsequent test code 40 | // gets ahead of the server's ability to record the request 41 | setTimeout(() => { resolve(''); }, 250); 42 | socket.on('error', reject); 43 | }); 44 | } 45 | 46 | module.exports = { send, fireAndForget }; 47 | -------------------------------------------------------------------------------- /mbTest/cli/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "xunit, spec", 3 | "xunitReporterOptions": { 4 | "output": "testResults/cli/results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mbTest/cli/dataStringify/imposters.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "imposters": [ 3 | <%- include('services/data.ejs') -%> 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /mbTest/cli/dataStringify/services/body.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "injectedValue": "<%- data.testValue %>" 4 | } 5 | -------------------------------------------------------------------------------- /mbTest/cli/dataStringify/services/data.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http", 3 | "port": 4542, 4 | "stubs": [ 5 | { 6 | "responses": [ 7 | { 8 | "is": { 9 | "headers": { 10 | "Content-Type": "application/json" 11 | }, 12 | "body": "<%- stringify(filename, 'services/body.ejs', {testValue: '1111'}) %>" 13 | } 14 | }, 15 | { 16 | "is": { 17 | "headers": { 18 | "Content-Type": "application/json" 19 | }, 20 | "body": "<%- stringify(filename, 'services/body.ejs', {testValue: '2222'}) %>" 21 | } 22 | }, 23 | { 24 | "is": { 25 | "headers": { 26 | "Content-Type": "application/json" 27 | }, 28 | "body": "<%- stringify(filename, 'services/body.ejs', {testValue: '3333'}) %>" 29 | } 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /mbTest/cli/formatters/asyncBase64Formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Demonstrates a silly custom formatter that saves the file as base64 encoding 5 | */ 6 | 7 | function encode (obj) { 8 | return Buffer.from(JSON.stringify(obj)).toString('base64'); 9 | } 10 | 11 | function decode (text) { 12 | return Buffer.from(text, 'base64').toString('utf8'); 13 | } 14 | 15 | function load (options) { 16 | const fs = require('fs-extra'); 17 | 18 | return new Promise((resolve, reject) => { 19 | fs.readFile(options.configfile, { encoding: 'utf8' }, (err, data) => { 20 | if (err) { 21 | reject(err); 22 | } 23 | else { 24 | resolve(JSON.parse(decode(data))); 25 | } 26 | }); 27 | }); 28 | } 29 | 30 | function save (options, imposters) { 31 | const fs = require('fs-extra'); 32 | 33 | return new Promise((resolve, reject) => { 34 | fs.writeFile(options.savefile, encode(imposters), err => { 35 | if (err) { 36 | reject(err); 37 | } 38 | else { 39 | resolve(); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | module.exports = { load, save }; 46 | -------------------------------------------------------------------------------- /mbTest/cli/formatters/base64Formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Demonstrates a silly custom formatter that saves the file as base64 encoding 5 | */ 6 | 7 | function encode (obj) { 8 | return Buffer.from(JSON.stringify(obj)).toString('base64'); 9 | } 10 | 11 | function decode (text) { 12 | return Buffer.from(text, 'base64').toString('utf8'); 13 | } 14 | 15 | function load (options) { 16 | const fs = require('fs-extra'), 17 | contents = fs.readFileSync(options.configfile, { encoding: 'utf8' }); 18 | return JSON.parse(decode(contents)); 19 | } 20 | 21 | function save (options, imposters) { 22 | const fs = require('fs-extra'); 23 | 24 | if (options.customName && imposters.imposters.length > 0) { 25 | imposters.imposters[0].name = options.customName; 26 | } 27 | fs.writeFileSync(options.savefile, encode(imposters)); 28 | } 29 | 30 | module.exports = { load, save }; 31 | -------------------------------------------------------------------------------- /mbTest/cli/gzip.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4542, 3 | "protocol": "http", 4 | "name": "gzip request", 5 | "stubs": [ 6 | { 7 | "responses": [ 8 | { 9 | "is": { 10 | "headers": { 11 | "Content-Type": "application/json" 12 | }, 13 | "body": { 14 | "code": "SUCCESS", 15 | "author": "J.K. Rowling" 16 | } 17 | } 18 | } 19 | ], 20 | "predicates": [ 21 | { 22 | "equals": { 23 | "body": { 24 | "title": "Harry Potter" 25 | } 26 | }, 27 | "caseSensitive": true, 28 | "comment": "case sensitivity applies to the key as well as the value" 29 | } 30 | ] 31 | }, 32 | { 33 | "responses": [ 34 | { 35 | "is": { 36 | "headers": { 37 | "Content-Type": "application/json" 38 | }, 39 | "body": { 40 | "code": "FAILED TO MATCH" 41 | } 42 | } 43 | } 44 | ] 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /mbTest/cli/imposters/account.xml: -------------------------------------------------------------------------------- 1 | 2 | 123 3 | Test 4 | 5 | -------------------------------------------------------------------------------- /mbTest/cli/imposters/accounts.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 5555, 3 | "protocol": "https", 4 | "name": "account service", 5 | "stubs": [ 6 | { 7 | "responses": [ 8 | { 9 | "is": { 10 | "statusCode": 401, 11 | "headers": { 12 | "www-authenticate": "Basic realm=\"secret\"" 13 | } 14 | } 15 | } 16 | ], 17 | "predicates": [ 18 | { 19 | "exists": { 20 | "headers": { 21 | "authorization": false 22 | } 23 | } 24 | } 25 | ] 26 | }, 27 | { 28 | "responses": [ 29 | { 30 | "is": { 31 | "body": "<%- stringify(filename, 'account.xml') %>" 32 | } 33 | } 34 | ], 35 | "predicates": [ 36 | { 37 | "equals": { 38 | "path": "/accounts/123", 39 | "method": "GET" 40 | } 41 | } 42 | ] 43 | }, 44 | { 45 | "responses": [ 46 | { 47 | "is": { 48 | "statusCode": 404 49 | } 50 | } 51 | ], 52 | "predicates": [ 53 | { 54 | "not": { 55 | "equals": { 56 | "path": "/accounts/123", 57 | "method": "GET" 58 | } 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /mbTest/cli/imposters/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 6565, 3 | "protocol": "smtp", 4 | "name": "email server" 5 | } 6 | -------------------------------------------------------------------------------- /mbTest/cli/imposters/imposters.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "imposters": [ 3 | <%- include('accounts.json') -%>, 4 | <%- include('email.json') -%>, 5 | <%- include('orders.json') -%>, 6 | <%- include('users.json') -%> 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /mbTest/cli/imposters/orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4545, 3 | "protocol": "http", 4 | "name": "order service", 5 | "stubs": [ 6 | { 7 | "responses": [ 8 | { 9 | "is": { 10 | "statusCode": 201, 11 | "headers": { 12 | "location": "http://localhost:4545/orders/123" 13 | } 14 | } 15 | }, 16 | { 17 | "is": { 18 | "statusCode": 201, 19 | "headers": { 20 | "location": "http://localhost:4545/orders/234" 21 | } 22 | } 23 | } 24 | ], 25 | "predicates": [ 26 | { 27 | "equals": { 28 | "path": "/orders", 29 | "method": "POST" 30 | } 31 | } 32 | ] 33 | }, 34 | { 35 | "responses": [ 36 | { 37 | "is": { 38 | "body": "Order 123" 39 | } 40 | } 41 | ], 42 | "predicates": [ 43 | { 44 | "equals": { 45 | "path": "/orders/123", 46 | "method": "GET" 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | "responses": [ 53 | { 54 | "is": { 55 | "body": "Order 234" 56 | } 57 | } 58 | ], 59 | "predicates": [ 60 | { 61 | "equals": { 62 | "path": "/orders/234", 63 | "method": "GET" 64 | } 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /mbTest/cli/imposters/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 7575, 3 | "protocol": "https", 4 | "name": "user service", 5 | "stubs": [ 6 | { 7 | "responses": [ 8 | { 9 | "is": { 10 | "body": "<%- stringify('users.xml') %>" 11 | } 12 | } 13 | ], 14 | "predicates": [ 15 | { 16 | "equals": { 17 | "path": "/users/123", 18 | "method": "GET" 19 | } 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /mbTest/cli/imposters/users.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 123 4 | Test Tester 5 | 6 | 7 | -------------------------------------------------------------------------------- /mbTest/cli/nestedStringify/imposters.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "imposters": [ 3 | <%- include('services/data.ejs') -%> 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /mbTest/cli/nestedStringify/services/body.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "success": true 3 | } 4 | -------------------------------------------------------------------------------- /mbTest/cli/nestedStringify/services/data.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http", 3 | "port": 4542, 4 | "stubs": [ 5 | { 6 | "responses": [ 7 | { 8 | "inject": "<%- stringify(filename, 'services/response.ejs') %>" 9 | } 10 | ] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /mbTest/cli/nestedStringify/services/response.ejs: -------------------------------------------------------------------------------- 1 | (request, state, logger) => { 2 | return { 3 | headers: { 4 | 'Content-Type': 'application/json' 5 | }, 6 | body: '<%- stringify(filename, 'services/body.ejs') %>' 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /mbTest/cli/noparse.json: -------------------------------------------------------------------------------- 1 | { 2 | "imposters": [ 3 | { 4 | "port": 4545, 5 | "protocol": "http", 6 | "stubs": [ 7 | { 8 | "responses": [ 9 | { 10 | "is": { 11 | "headers": { 12 | "Content-Type": "text/plain" 13 | }, 14 | "body": "<% should not render through ejs" 15 | } 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /mbTest/cli/replayTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | api = require('../api').create(), 5 | client = require('../baseHttpClient').create('http'), 6 | mb = require('../mb').create(api.port + 1), 7 | timeout = parseInt(process.env.MB_SLOW_TEST_TIMEOUT || 4000); 8 | 9 | describe('mb replay', function () { 10 | this.timeout(timeout); 11 | 12 | it('should remove proxies', async function () { 13 | const originServerPort = mb.port + 1, 14 | originServerFn = (request, state) => { 15 | state.count = state.count || 0; 16 | state.count += 1; 17 | return { 18 | body: `${state.count}. ${request.path}` 19 | }; 20 | }, 21 | originServerStub = { responses: [{ inject: originServerFn.toString() }] }, 22 | originServerRequest = { 23 | protocol: 'http', 24 | port: originServerPort, 25 | stubs: [originServerStub], 26 | name: 'origin server' 27 | }, 28 | proxyPort = mb.port + 2, 29 | proxyDefinition = { 30 | to: `http://localhost:${originServerPort}`, 31 | mode: 'proxyAlways', 32 | predicateGenerators: [{ matches: { path: true } }] 33 | }, 34 | proxyStub = { responses: [{ proxy: proxyDefinition }] }, 35 | proxyRequest = { protocol: 'http', port: proxyPort, stubs: [proxyStub], name: 'PROXY' }; 36 | await mb.start(['--allowInjection']); 37 | await mb.post('/imposters', originServerRequest); 38 | await mb.post('/imposters', proxyRequest); 39 | 40 | await client.get('/first', proxyPort); 41 | await client.get('/second', proxyPort); 42 | await client.get('/first', proxyPort); 43 | 44 | await mb.replay(); 45 | const response = await mb.get(`/imposters/${proxyPort}`), 46 | stubs = response.body.stubs, 47 | responses = stubs.map(stub => stub.responses.map(stubResponse => stubResponse.is.body)); 48 | 49 | try { 50 | assert.strictEqual(response.body.stubs.length, 2, JSON.stringify(response.body.stubs, null, 2)); 51 | assert.deepEqual(responses, [['1. /first', '3. /first'], ['2. /second']]); 52 | } 53 | finally { 54 | await mb.stop(); 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /mbTest/cli/templates/counter.ejs: -------------------------------------------------------------------------------- 1 | (request, state) => { 2 | const count = state.requests ? Object.keys(state.requests).length : 0; 3 | 4 | return { 5 | body: `There have been ${count} proxied calls` 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /mbTest/cli/templates/imposters.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "imposters": [ 3 | <%- include('originServer.ejs') -%>, 4 | <%- include('proxyServer.ejs') -%> 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /mbTest/cli/templates/originServer.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "port": 5555, 3 | "protocol": "http", 4 | "name": "origin", 5 | "stubs": [ 6 | { 7 | "responses": [{ "inject": "<%- stringify(filename, 'originServerResponse.ejs') %>" }] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /mbTest/cli/templates/originServerResponse.ejs: -------------------------------------------------------------------------------- 1 | (request, state, logger) => { 2 | logger.info('origin called'); 3 | state.requests = state.requests || 0; 4 | state.requests += 1; 5 | return { 6 | headers: { 7 | 'Content-Type': 'application/json' 8 | }, 9 | body: JSON.stringify({ count: state.requests }) 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /mbTest/cli/templates/proxy.ejs: -------------------------------------------------------------------------------- 1 | (request, state, logger, callback) => { 2 | const cacheKey = request.method + ' ' + request.path; 3 | 4 | if (typeof state.requests === 'undefined') { 5 | state.requests = {}; 6 | } 7 | 8 | if (state.requests[cacheKey]) { 9 | logger.info('Using previous response'); 10 | callback(state.requests[cacheKey]); 11 | } 12 | 13 | const http = require('http'), 14 | options = { 15 | method: request.method, 16 | hostname: 'localhost', 17 | port: 5555, 18 | path: request.path, 19 | headers: request.headers 20 | }, 21 | httpRequest = http.request(options, response => { 22 | let body = ''; 23 | response.setEncoding('utf8'); 24 | response.on('data', (chunk) => { 25 | body += chunk; 26 | }); 27 | response.on('end', () => { 28 | const stubResponse = { 29 | statusCode: response.statusCode, 30 | headers: response.headers, 31 | body 32 | }; 33 | logger.info('Successfully proxied: ' + JSON.stringify(stubResponse)); 34 | state.requests[cacheKey] = stubResponse; 35 | callback(stubResponse); 36 | }); 37 | }); 38 | httpRequest.end(); 39 | } 40 | -------------------------------------------------------------------------------- /mbTest/cli/templates/proxyServer.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "port": 4546, 3 | "protocol": "http", 4 | "name": "proxy", 5 | "stubs": [ 6 | { 7 | "responses": [{ "inject": "<%- stringify(filename, 'counter.ejs') %>" }], 8 | "predicates": [{ 9 | "equals": { 10 | "method": "GET", 11 | "path": "/counter" 12 | } 13 | }] 14 | }, 15 | { 16 | "responses": [{ "inject": "<%- inject(filename, 'proxy.ejs') %>" }] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /mbTest/js/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "xunit, spec", 3 | "xunitReporterOptions": { 4 | "output": "testResults/api/results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mbTest/js/createAppTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | express = require('express'), 5 | createApp = require('mountebank').createApp, 6 | httpClient = require('../baseHttpClient').create('http'), 7 | headers = { connection: 'close' }; // prevent hanging on to connections, delaying close 8 | 9 | describe('Integration with existing server', function () { 10 | let server; 11 | let port; 12 | 13 | async function listen (app) { 14 | return new Promise((resolve, reject) => { 15 | server = app.listen(0, () => { 16 | resolve(server.address().port); 17 | }); 18 | 19 | server.on('error', e => reject(e)); 20 | }); 21 | } 22 | 23 | async function close () { 24 | return new Promise(resolve => server.close(resolve)); 25 | } 26 | 27 | beforeEach(async function () { 28 | const app = express(); 29 | const mbApp = await createApp({ debug: true }); 30 | 31 | app.get('/', (req, res) => { 32 | res.status(200).send('ok'); 33 | }); 34 | 35 | app.use('/mountebank', (req, res) => { 36 | mbApp(req, res); 37 | }); 38 | 39 | port = await listen(app); 40 | }); 41 | 42 | afterEach(async function () { 43 | await httpClient.del('/mountebank/imposters', port, headers); 44 | await close(); 45 | }); 46 | 47 | it('should success respond on application path', async function () { 48 | const response = await httpClient.get('/', port, headers); 49 | 50 | assert.strictEqual(response.body, 'ok'); 51 | }); 52 | 53 | it('should success respond on mountebank application path', async function () { 54 | const response = await httpClient.get('/mountebank/config', port, headers); 55 | 56 | assert.ok(response.body.options.debug); 57 | }); 58 | 59 | it('should return create new imposter with consistent hypermedia', async function () { 60 | const creationResponse = await httpClient.post('/mountebank/imposters', { protocol: 'http' }, port, headers); 61 | const imposter = creationResponse.body; 62 | 63 | assert.strictEqual(creationResponse.statusCode, 201, JSON.stringify(imposter, null, 2)); 64 | assert.strictEqual(creationResponse.headers.location, imposter._links.self.href); 65 | 66 | const imposterResponse = await httpClient.get(`/mountebank/imposters/${imposter.port}`, port, headers); 67 | 68 | assert.strictEqual(imposterResponse.statusCode, 200); 69 | assert.deepEqual(imposter, creationResponse.body); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /mbTest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mountebank-tests", 3 | "version": "0.0.1", 4 | "author": "Brandon Byars ", 5 | "private": true, 6 | "description": "Black box tests for mountebank", 7 | "scripts": { 8 | "api": "mocha --forbid-only --forbid-pending --reporter mocha-multi-reporters --reporter-options configFile=api/config.json 'api/**/*.js'", 9 | "cli": "mocha --forbid-only --forbid-pending --reporter mocha-multi-reporters --reporter-options configFile=cli/config.json 'cli/**/*.js'", 10 | "js": "mocha --forbid-only --forbid-pending --reporter mocha-multi-reporters --reporter-options configFile=cli/config.json 'js/**/*.js'", 11 | "perf": "mocha --forbid-only --forbid-pending --reporter mocha-multi-reporters --reporter-options configFile=perf/config.json 'perf/**/*.js'", 12 | "web": "mocha --forbid-only --forbid-pending --reporter mocha-multi-reporters --reporter-options configFile=web/config.json 'web/**/*.js'", 13 | "test": "npm run api && npm run cli && npm run js", 14 | "airplane": "MB_AIRPLANE_MODE=true npm test" 15 | }, 16 | "dependencies": { 17 | "@xmldom/xmldom": "0.8.10", 18 | "express": "4.18.2", 19 | "fs-extra": "11.2.0", 20 | "hpagent": "1.0.0", 21 | "jsdom": "24.0.0", 22 | "mocha": "10.2.0", 23 | "mocha-multi-reporters": "1.5.1", 24 | "mountebank": "file:..", 25 | "nc": "1.0.3", 26 | "nodemailer": "6.9.4", 27 | "safe-stable-stringify": "2.4.3", 28 | "w3cjs": "0.4.0", 29 | "xpath": "0.0.34" 30 | }, 31 | "engines": { 32 | "node": ">=16" 33 | }, 34 | "files": [ 35 | "api", 36 | "cli", 37 | "js", 38 | "perf", 39 | "web", 40 | "api.js", 41 | "baseHttpClient.js", 42 | "mb.js", 43 | "package.json", 44 | "package-lock.json" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /mbTest/perf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "xunit, spec", 3 | "xunitReporterOptions": { 4 | "output": "testResults/perf/results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mbTest/perf/scenarios/baseline.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http", 3 | "port": 3000 4 | } 5 | -------------------------------------------------------------------------------- /mbTest/perf/scenarios/singleStub.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http", 3 | "port": 3000, 4 | "stubs": [{ 5 | "predicates": [{ 6 | "equals": { "path": "/test" } 7 | }], 8 | "responses": [ 9 | { "is": { "body": "1" } }, 10 | { "is": { "body": "2" } }, 11 | { "is": { "body": "3" } }, 12 | { "is": { "body": "4" } }, 13 | { "is": { "body": "5" } }, 14 | { "is": { "body": "6" } }, 15 | { "is": { "body": "7" } }, 16 | { "is": { "body": "8" } }, 17 | { "is": { "body": "9" } }, 18 | { "is": { "body": "10" } } 19 | ] 20 | }] 21 | } 22 | -------------------------------------------------------------------------------- /mbTest/web/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "xunit, spec", 3 | "xunitReporterOptions": { 4 | "output": "testResults/web/results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mbTest/web/contractTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | api = require('../api').create(), 5 | JSDOM = require('jsdom').JSDOM, 6 | timeout = parseInt(process.env.MB_SLOW_TEST_TIMEOUT || 3000); 7 | 8 | async function getDOM (endpoint) { 9 | const url = api.url + endpoint, 10 | dom = await JSDOM.fromURL(url); 11 | return dom.window; 12 | } 13 | 14 | async function getJSONFor (contract) { 15 | const window = await getDOM('/docs/api/contracts'); 16 | return window.document.getElementById(`${contract}-specification`).innerHTML.replace(/<[^>]+>/g, ''); 17 | } 18 | 19 | function assertJSON (json) { 20 | try { 21 | JSON.parse(json); 22 | } 23 | catch (e) { 24 | assert.fail(`${json}\n${e}`); 25 | } 26 | } 27 | 28 | describe('contracts', function () { 29 | this.timeout(timeout); 30 | 31 | ['home', 'imposters', 'imposter', 'config', 'logs'].forEach(contractType => { 32 | it(`${contractType} contract should be valid JSON`, async function () { 33 | const json = await getJSONFor(contractType); 34 | assertJSON(json); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /mbTest/web/crawlerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | api = require('../api').create(), 5 | crawler = require('./crawler'), 6 | expectedContentType = contentType => { 7 | if (!contentType) { 8 | return true; 9 | } 10 | return ['text/html', 'application/atom+xml', 'text/plain', 'application/octet-stream'].some(type => contentType.indexOf(type) >= 0); 11 | }, 12 | isLocalLink = link => link.indexOf(api.url) === 0, 13 | isBookLink = link => link.indexOf('manning.com') > 0, 14 | expectedStatusCode = (link, statusCode) => 15 | // The 999 Request Denied code started coming from Slideshare 16 | // It works locally but fails on TravisCI. I tried spoofing with a chrome user agent, 17 | // but it still failed on Travis, so there's some clever spider detection they're doing. 18 | // Added 50x codes to make test less brittle - those have been ephemeral errors, as 19 | // long as they're not part of the mb site itself 20 | [200, 301, 302, 999].indexOf(statusCode) >= 0 21 | || ([500, 502, 503].indexOf(statusCode) >= 0 && isLocalLink(link)) 22 | || (statusCode === 403 && isBookLink(link)); 23 | 24 | describe('The mountebank website', function () { 25 | this.timeout(180000); 26 | 27 | it('should have no dead links and a valid sitemap', async function () { 28 | const crawlResults = await crawler.create().crawl(`${api.url}/`, ''), 29 | errors = { misses: {} }; // Validate no broken links 30 | 31 | errors.errors = crawlResults.errors; 32 | Object.keys(crawlResults.hits).forEach(link => { 33 | if (!expectedStatusCode(link, crawlResults.hits[link].statusCode) || 34 | !expectedContentType(crawlResults.hits[link].contentType)) { 35 | errors.misses[link] = crawlResults.hits[link]; 36 | } 37 | }); 38 | 39 | assert.deepEqual(errors, { errors: [], misses: {} }, JSON.stringify(errors, null, 4)); 40 | 41 | const response = await api.get('/sitemap'), 42 | siteLinks = Object.keys(crawlResults.hits) 43 | .filter(link => isLocalLink(link) && link.indexOf('#') < 0 && link.indexOf('?') < 0) 44 | .map(link => link.replace(api.url, 'http://www.mbtest.org')), 45 | linksNotInSitemap = siteLinks.filter(link => response.body.indexOf(link) < 0); 46 | 47 | assert.strictEqual(200, response.statusCode); 48 | assert.deepEqual(linksNotInSitemap, [], JSON.stringify(linksNotInSitemap)); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /mbTest/web/docsIntegrityTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isPersistent = process.env.MB_PERSISTENT === 'true', 4 | docs = require('./docsTester/docs'), 5 | isWindows = require('os').platform().indexOf('win') === 0, 6 | timeout = parseInt(process.env.MB_SLOW_TEST_TIMEOUT || 6000), 7 | fs = require('fs-extra'); 8 | 9 | function isInProcessImposter (protocol) { 10 | if (fs.existsSync('protocols.json')) { 11 | const protocols = require(process.cwd() + '/protocols.json'); 12 | return Object.keys(protocols).indexOf(protocol) < 0; 13 | } 14 | else { 15 | return true; 16 | } 17 | } 18 | 19 | async function validateDocs (page) { 20 | it(`${page} should be up-to-date`, async function () { 21 | const testScenarios = await docs.getScenarios(page), 22 | tests = Object.keys(testScenarios).map(testName => testScenarios[testName].assertValid()); 23 | return Promise.all(tests); 24 | }); 25 | } 26 | 27 | describe('docs', function () { 28 | this.timeout(timeout); 29 | 30 | [ 31 | '/docs/api/mocks', 32 | '/docs/api/proxies', 33 | '/docs/api/injection', 34 | '/docs/api/xpath', 35 | '/docs/api/json', 36 | '/docs/protocols/https', 37 | '/docs/protocols/http', 38 | '/docs/api/jsonpath' 39 | ].forEach(page => { 40 | validateDocs(page); 41 | }); 42 | 43 | // The logs change for out of process imposters 44 | if (isInProcessImposter('tcp')) { 45 | validateDocs('/docs/api/overview'); 46 | } 47 | 48 | // For tcp out of process imposters or using the --datadir option, I can't get the netcat tests working, 49 | // even with a -q1 replacement. The nc client ends the socket connection 50 | // before the server has a chance to respond. 51 | if (isInProcessImposter('tcp') && !isWindows && !isPersistent) { 52 | [ 53 | '/docs/gettingStarted', 54 | '/docs/api/predicates', 55 | '/docs/api/behaviors', 56 | '/docs/api/stubs', 57 | '/docs/protocols/tcp' 58 | ].forEach(page => { 59 | validateDocs(page); 60 | }); 61 | } 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /mbTest/web/docsTester/docsTestScenario.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function create (endpoint, id) { 4 | const steps = []; 5 | 6 | function addStep (stepSpec) { 7 | const step = { 8 | assertValid: stepSpec.assertValid, 9 | execute: async () => { 10 | const runner = require(`./testTypes/${stepSpec.type}`), 11 | actualResponse = await runner.runStep(stepSpec); 12 | step.actualResponse = actualResponse; 13 | } 14 | }; 15 | 16 | steps.push(step); 17 | } 18 | 19 | async function assertValid () { 20 | const stepExecutions = steps.map(step => step.execute); 21 | let chainedExecutions = Promise.resolve(); 22 | 23 | stepExecutions.forEach(step => { 24 | chainedExecutions = chainedExecutions.then(step); 25 | }); 26 | 27 | await chainedExecutions; 28 | steps.forEach((step, stepIndex) => { 29 | const failureMessage = `${endpoint} ${id} step ${stepIndex + 1} failed; below is the actual result\n` + 30 | '-----------\n' + 31 | `${step.actualResponse}\n` + 32 | '-----------'; 33 | 34 | step.assertValid(step.actualResponse, failureMessage); 35 | }); 36 | } 37 | 38 | return { addStep, assertValid }; 39 | } 40 | 41 | module.exports = { create }; 42 | -------------------------------------------------------------------------------- /mbTest/web/docsTester/testTypes/exec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exec = require('child_process').exec, 4 | fs = require('fs-extra'); 5 | let nextTestId = 1; 6 | 7 | function execute (command) { 8 | return new Promise((resolve, reject) => { 9 | exec(command, (error, stdout) => { 10 | if (error) { 11 | reject(error); 12 | } 13 | else { 14 | resolve(stdout); 15 | } 16 | }); 17 | }); 18 | } 19 | 20 | function usePortableNetcat (command) { 21 | // On CircleCI image, netcat requires the -q1 parameter to end after receiving stdin. 22 | // Faster machines don't need it. 23 | // I could not find a centos version of netcat that has the -q parameter, nor does 24 | // the Mac version have the -q parameter. The -w1 parameter, which is cross-OS, did not 25 | // work. The only solution I found was to use the node port of netcat and use it :( 26 | // This feels ugly to do the replacement here, but I prefer it over doing the replacement 27 | // directly in the views because I want people to be able to copy the command from the 28 | // public site and run on their machines, and the variant without the -q is the most portable 29 | return command.replace('| nc ', '| npx nc -q1 '); 30 | } 31 | 32 | async function runStep (step) { 33 | const filename = `test-${nextTestId}`; 34 | 35 | fs.writeFileSync(filename, usePortableNetcat(step.requestText), { mode: 484 /* 0744 */ }); 36 | nextTestId += 1; 37 | 38 | try { 39 | return await execute(`sh ./${filename}`); 40 | } 41 | catch (reason) { 42 | console.log(`Error executing following command: ${step.text}`); 43 | throw reason; 44 | } 45 | finally { 46 | fs.unlinkSync(filename); 47 | } 48 | } 49 | 50 | module.exports = { runStep }; 51 | -------------------------------------------------------------------------------- /mbTest/web/docsTester/testTypes/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | 5 | function runStep (config) { 6 | if (config.delete === 'true') { 7 | fs.unlinkSync(config.filename); 8 | } 9 | else { 10 | fs.writeFileSync(config.filename, config.requestText); 11 | } 12 | return Promise.resolve(config); 13 | } 14 | 15 | module.exports = { runStep }; 16 | -------------------------------------------------------------------------------- /mbTest/web/docsTester/testTypes/smtp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const smtpClient = require('../../../api/smtp/smtpClient'); 4 | 5 | function camelCase (key) { 6 | return key.substring(0, 1).toLowerCase() + key.substring(1); 7 | } 8 | 9 | function parseHeader (line) { 10 | const parts = line.split(':'); 11 | return { 12 | key: camelCase(parts[0].trim()), 13 | value: parts.slice(1).join(':').trim() 14 | }; 15 | } 16 | 17 | function parse (text) { 18 | const lines = text.split(/\r?\n/), 19 | message = { to: [], cc: [], bcc: [] }; 20 | 21 | for (var i = 0; i < lines.length; i += 1) { 22 | if (lines[i].trim() === '') { 23 | break; 24 | } 25 | const header = parseHeader(lines[i]); 26 | if (Array.isArray(message[header.key])) { 27 | message[header.key].push(header.value); 28 | } 29 | else { 30 | message[header.key] = header.value; 31 | } 32 | } 33 | message.text = lines.slice(i).join('\n').trim(); 34 | return message; 35 | } 36 | 37 | async function runStep (step) { 38 | const message = parse(step.requestText); 39 | await smtpClient.send(message, step.port); 40 | } 41 | 42 | module.exports = { runStep }; 43 | -------------------------------------------------------------------------------- /mbTest/web/feedTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | api = require('../api').create(), 5 | httpClient = require('../baseHttpClient').create('http'), 6 | xpath = require('xpath'), 7 | DOMParser = require('@xmldom/xmldom').DOMParser, 8 | timeout = parseInt(process.env.MB_SLOW_TEST_TIMEOUT || 3000); 9 | 10 | function entryCount (body) { 11 | const doc = new DOMParser().parseFromString(body), 12 | select = xpath.useNamespaces({ atom: 'http://www.w3.org/2005/Atom' }); 13 | return select('count(//atom:entry)', doc); 14 | } 15 | 16 | function getNextLink (body) { 17 | const doc = new DOMParser().parseFromString(body), 18 | select = xpath.useNamespaces({ atom: 'http://www.w3.org/2005/Atom' }); 19 | return select('//atom:link[@rel="next"]/@href', doc)[0].value; 20 | } 21 | 22 | describe('the feed', function () { 23 | this.timeout(timeout); 24 | 25 | it('should default to page 1 with 10 entries', async function () { 26 | const feedResponse = await httpClient.get('/feed', api.port); 27 | assert.strictEqual(feedResponse.statusCode, 200); 28 | assert.strictEqual(feedResponse.headers['content-type'], 'application/atom+xml; charset=utf-8'); 29 | assert.strictEqual(entryCount(feedResponse.body), 10); 30 | 31 | const nextPageResponse = await httpClient.get(getNextLink(feedResponse.body), api.port); 32 | assert.strictEqual(nextPageResponse.statusCode, 200); 33 | assert.ok(entryCount(nextPageResponse.body) > 0, 'No entries'); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /releases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "version": "v1.1.0", "date": "2014-05-09" }, 3 | { "version": "v1.1.36", "date": "2014-05-20" }, 4 | { "version": "v1.1.72", "date": "2014-12-31" }, 5 | { "version": "v1.2.0", "date": "2015-01-03" }, 6 | { "version": "v1.2.30", "date": "2015-01-24" }, 7 | { "version": "v1.2.45", "date": "2015-02-16" }, 8 | { "version": "v1.2.56", "date": "2015-03-14" }, 9 | { "version": "v1.2.103", "date": "2015-04-12" }, 10 | { "version": "v1.2.122", "date": "2015-05-04" }, 11 | { "version": "v1.3.0", "date": "2015-09-20" }, 12 | { "version": "v1.3.1", "date": "2015-10-11" }, 13 | { "version": "v1.4.0", "date": "2015-10-14" }, 14 | { "version": "v1.4.1", "date": "2015-10-23" }, 15 | { "version": "v1.4.2", "date": "2015-11-07" }, 16 | { "version": "v1.4.3", "date": "2016-01-16" }, 17 | { "version": "v1.5.0", "date": "2016-04-11" }, 18 | { "version": "v1.5.1", "date": "2016-04-18" }, 19 | { "version": "v1.6.0", "date": "2016-08-15" }, 20 | { "version": "v1.7.0", "date": "2016-11-29" }, 21 | { "version": "v1.7.1", "date": "2016-11-30" }, 22 | { "version": "v1.7.2", "date": "2016-12-2" }, 23 | { "version": "v1.8.0", "date": "2017-01-03" }, 24 | { "version": "v1.9.0", "date": "2017-02-26" }, 25 | { "version": "v1.10.0", "date": "2017-04-02" }, 26 | { "version": "v1.11.0", "date": "2017-05-20" }, 27 | { "version": "v1.12.0", "date": "2017-07-22" }, 28 | { "version": "v1.13.0", "date": "2017-10-27" }, 29 | { "version": "v1.14.0", "date": "2018-02-18" }, 30 | { "version": "v1.14.1", "date": "2018-05-16" }, 31 | { "version": "v1.15.0", "date": "2018-08-26" }, 32 | { "version": "v1.16.0", "date": "2018-12-21" }, 33 | { "version": "v2.0.0", "date": "2019-03-15" }, 34 | { "version": "v2.1.0", "date": "2019-06-19" }, 35 | { "version": "v2.1.1", "date": "2019-10-13" }, 36 | { "version": "v2.1.2", "date": "2019-10-19" }, 37 | { "version": "v2.2.0", "date": "2020-02-09" }, 38 | { "version": "v2.2.1", "date": "2020-04-26" }, 39 | { "version": "v2.3.0", "date": "2020-09-07" }, 40 | { "version": "v2.3.1", "date": "2020-09-26" }, 41 | { "version": "v2.3.2", "date": "2020-09-27" }, 42 | { "version": "v2.3.3", "date": "2020-10-18" }, 43 | { "version": "v2.4.0", "date": "2020-12-29" }, 44 | { "version": "v2.5.0", "date": "2021-09-15" }, 45 | { "version": "v2.6.0", "date": "2022-02-20" }, 46 | { "version": "v2.7.0", "date": "2022-07-17" }, 47 | { "version": "v2.8.0", "date": "2022-10-16" }, 48 | { "version": "v2.8.1", "date": "2022-10-16" }, 49 | { "version": "v2.8.2", "date": "2023-01-22" }, 50 | { "version": "v2.9.0", "date": "2023-08-23" }, 51 | { "version": "v2.9.1", "date": "2023-08-23" } 52 | ] 53 | -------------------------------------------------------------------------------- /scripts/codeclimate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -e ./cc-test-reporter ]; then 4 | if [ `uname` = "Darwin" ]; then 5 | OS=darwin 6 | else 7 | OS=linux 8 | fi 9 | 10 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-${OS}-amd64 > ./cc-test-reporter 11 | chmod +x ./cc-test-reporter 12 | fi 13 | 14 | ./cc-test-reporter before-build 15 | npm run test:cover 16 | ./cc-test-reporter after-build --coverage-input-type lcov --exit-code $? 17 | -------------------------------------------------------------------------------- /scripts/printVersion: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | const thisPackage = require('../dist/mountebank/package.json'); 5 | console.log(thisPackage.version); 6 | 7 | -------------------------------------------------------------------------------- /scripts/publishDocker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | VERSION=$1 6 | TAG=beta 7 | if [ "$MB_RELEASE" = "true" ]; then 8 | TAG=latest 9 | fi 10 | 11 | cd dist/mountebank 12 | docker context create mountebank 13 | docker buildx create mountebank --use 14 | # https://stackoverflow.com/questions/72167570/docker-buildx-nodejs-fail 15 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 16 | docker login --username $DOCKER_USER --password "$DOCKER_PASSWORD" 17 | docker buildx build --platform=linux/arm64,linux/amd64 --no-cache --tag bbyars/mountebank:$MB_VERSION --tag bbyars/mountebank:$TAG --push . 18 | 19 | -------------------------------------------------------------------------------- /scripts/publishHeroku: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | APP_NAME=$1 6 | 7 | if [[ $(command -v heroku) = "" ]]; then 8 | echo "Installing heroku CLI" 9 | curl https://cli-assets.heroku.com/install.sh | sh 10 | fi 11 | 12 | cd dist/mountebank 13 | 14 | # $PORT is set by Heroku 15 | mv Dockerfile Dockerfile.old 16 | cat Dockerfile.old | sed -E -e 's/ENTRYPOINT \["mb"\]/CMD mb start --port $PORT/' > Dockerfile 17 | 18 | heroku container:login 19 | heroku container:push -a $APP_NAME web 20 | heroku container:release -a $APP_NAME web 21 | 22 | mv Dockerfile.old Dockerfile 23 | -------------------------------------------------------------------------------- /scripts/publishNpm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f ~/.npmrc ] && [ -n "$NPM_API_KEY" ]; then 4 | echo //registry.npmjs.org/:_authToken=$NPM_API_KEY > ~/.npmrc 5 | fi 6 | 7 | TAG=beta 8 | if [ "$MB_RELEASE" = "true" ]; then 9 | TAG=latest 10 | fi 11 | 12 | cd dist/mountebank 13 | npm publish . --tag $TAG 14 | -------------------------------------------------------------------------------- /scripts/sonar: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ `uname` = "Darwin" ]; then 6 | OS=macosx 7 | else 8 | OS=linux 9 | fi 10 | 11 | VERSION=5.0.1.3006 12 | 13 | echo "Downloading sonar-scanner..." 14 | wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${VERSION}-${OS}.zip 15 | unzip sonar-scanner-cli-${VERSION}-${OS}.zip 16 | rm sonar-scanner-cli-${VERSION}-${OS}.zip 17 | 18 | # Necessary for sonar-scanner to run correctly 19 | PATH=$PATH:`pwd`/sonar-scanner-${VERSION}-${OS}/bin 20 | echo sonar.host.url=https://sonarcloud.io >> sonar-scanner-${VERSION}-${OS}/conf/sonar-scanner.properties 21 | 22 | echo "Running sonar-scanner..." 23 | sonar-scanner \ 24 | -Dsonar.projectKey=mountebank \ 25 | -Dsonar.projectName=mountebank \ 26 | -Dsonar.links.homepage=http://www.mbtest.org \ 27 | -Dsonar.links.ci=https://circleci.com/gh/bbyars/workflows/mountebank \ 28 | -Dsonar.links.issue=https://github.com/bbyars/mountebank/issues \ 29 | -Dsonar.links.scm=https://github.com/bbyars/mountebank \ 30 | -Dsonar.sources=src \ 31 | -Dsonar.exclusions=src/public/scripts/jquery \ 32 | -Dsonar.tests=test,mbTest \ 33 | -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \ 34 | -Dsonar.host.url=https://sonarcloud.io \ 35 | -Dsonar.organization=bbyars-github \ 36 | -Dsonar.token=$SONARQUBE_TOKEN 37 | 38 | rm -rf sonar-scanner-${VERSION}-${OS} 39 | -------------------------------------------------------------------------------- /src/controllers/configController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const helpers = require('../util/helpers'); 4 | 5 | /** 6 | * The controller that exposes the mountebank configuration for the running process 7 | * @module 8 | */ 9 | 10 | /** 11 | * Creates the config controller 12 | * @param {string} version - The version of the currently running process 13 | * @param {Object} options - The command line options used to start mb 14 | * @returns {Object} 15 | */ 16 | function create (version, options) { 17 | const publicOptions = helpers.clone(options); 18 | 19 | delete publicOptions.version; 20 | 21 | if (!publicOptions.mock) { 22 | delete publicOptions.mock; // deprecated 23 | } 24 | 25 | // On some OS's, it duplicates camelCase as hypen-case (e.g. noParse and no-parse) 26 | // I assume this was a change in yargs at some point 27 | for (var prop in publicOptions) { 28 | if (prop.indexOf('-') > 0) { 29 | delete publicOptions[prop]; 30 | } 31 | } 32 | 33 | /** 34 | * The method that responds to GET /config 35 | * @memberOf module:controllers/configController# 36 | * @param {Object} request - The HTTP request 37 | * @param {Object} response - The HTTP response 38 | */ 39 | function get (request, response) { 40 | const config = { 41 | version, 42 | options: publicOptions, 43 | process: { 44 | nodeVersion: process.version, 45 | architecture: process.arch, 46 | platform: process.platform, 47 | rss: process.memoryUsage().rss, 48 | heapTotal: process.memoryUsage().heapTotal, 49 | heapUsed: process.memoryUsage().heapUsed, 50 | uptime: process.uptime(), 51 | cwd: process.cwd() 52 | } 53 | }; 54 | 55 | response.format({ 56 | json: () => response.send(config), 57 | html: () => response.render('config', config) 58 | }); 59 | } 60 | 61 | return { get }; 62 | } 63 | 64 | module.exports = { create }; 65 | -------------------------------------------------------------------------------- /src/controllers/homeController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const date = require('../util/date.js'); 4 | 5 | /** 6 | * The controller that returns the base mountebank hypermedia 7 | * @module 8 | */ 9 | 10 | /** 11 | * Creates the home controller 12 | * @param {Object} releases - The releases.json file 13 | * @returns {Object} The controller 14 | */ 15 | function create (releases) { 16 | function createNotice (release) { 17 | return { 18 | version: release.version, 19 | when: date.howLongAgo(release.date) 20 | }; 21 | } 22 | 23 | function isRecent (notice) { 24 | return notice.when !== ''; 25 | } 26 | 27 | /** 28 | * The function that responds to GET / 29 | * @memberOf module:controllers/homeController# 30 | * @param {Object} request - the HTTP request 31 | * @param {Object} response - the HTTP response 32 | */ 33 | function get (request, response) { 34 | const hypermedia = { 35 | _links: { 36 | imposters: { href: '/imposters' }, 37 | config: { href: '/config' }, 38 | logs: { href: '/logs' } 39 | } 40 | }, 41 | notices = releases.map(createNotice).filter(isRecent), 42 | viewNotices = []; 43 | 44 | if (notices.length > 0) { 45 | notices.reverse(); 46 | viewNotices.push(notices[0]); 47 | } 48 | 49 | response.format({ 50 | json: () => { response.send(hypermedia); }, 51 | html: () => { response.render('index', { notices: viewNotices }); } 52 | }); 53 | } 54 | 55 | return { get }; 56 | } 57 | 58 | module.exports = { create }; 59 | -------------------------------------------------------------------------------- /src/controllers/logsController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'), 4 | escapeHtml = require('escape-html'); 5 | 6 | /** 7 | * The controller that exposes the logs 8 | * @module 9 | */ 10 | 11 | /** 12 | * Creates the logs controller 13 | * @param {string} logfile - the path to the logfile 14 | * @returns {{get: get}} 15 | */ 16 | function create (logfile) { 17 | function getLogEntries () { 18 | if (!logfile || !fs.existsSync(logfile)) { 19 | return [{ level: 'error', message: 'No logfile' }]; 20 | } 21 | try { 22 | const entries = fs.readFileSync(logfile).toString().split(/\r?\n/), 23 | json = '[' + entries.join(',').replace(/,$/, '') + ']'; 24 | return JSON.parse(json); 25 | } 26 | catch (ex) { 27 | return [{ level: 'error', message: 'This page only works for JSON file logging' }]; 28 | } 29 | } 30 | 31 | /** 32 | * The function that responds to GET /logs 33 | * @memberOf module:controllers/logsController# 34 | * @param {Object} request - the HTTP request 35 | * @param {Object} response - the HTTP response 36 | */ 37 | function get (request, response) { 38 | const allLogs = getLogEntries(), 39 | startIndex = parseInt(request.query.startIndex || 0), 40 | endIndex = parseInt(request.query.endIndex || allLogs.length - 1), 41 | logs = allLogs.slice(startIndex, endIndex + 1); 42 | 43 | response.format({ 44 | json: () => response.send({ logs: logs }), 45 | html: () => response.render('logs', { logs: logs, escape: escapeHtml }) 46 | }); 47 | } 48 | 49 | return { get }; 50 | } 51 | 52 | module.exports = { create }; 53 | -------------------------------------------------------------------------------- /src/models/http/httpServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'), 4 | baseHttpServer = require('./baseHttpServer.js'); 5 | 6 | /** 7 | * Represents an http imposter 8 | * @module 9 | */ 10 | 11 | function createBaseServer () { 12 | return { 13 | metadata: {}, 14 | createNodeServer: http.createServer 15 | }; 16 | } 17 | 18 | module.exports = baseHttpServer(createBaseServer); 19 | -------------------------------------------------------------------------------- /src/models/http/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = JSON.parse(process.argv[2]), 4 | httpServer = require('./httpServer.js'), 5 | httpProxy = require('./httpProxy.js'), 6 | mbConnection = require('../mbConnection.js').create(config); 7 | 8 | httpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => { 9 | mbConnection.setPort(server.port); 10 | mbConnection.setProxy(httpProxy.create(mbConnection.logger())); 11 | 12 | const metadata = server.metadata; 13 | metadata.port = server.port; 14 | console.log(JSON.stringify(metadata)); 15 | }).catch(error => { 16 | console.error(JSON.stringify(error)); 17 | process.exit(1); // eslint-disable-line no-process-exit 18 | }); 19 | -------------------------------------------------------------------------------- /src/models/https/cert/mb-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDMTCCAhkCFC1be7uOugfu2q1NmVapw9+SQ0skMA0GCSqGSIb3DQEBCwUAMFUx 3 | CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJUWDEPMA0GA1UEBwwGRGFsbGFzMRMwEQYD 4 | VQQKDAptb3VudGViYW5rMRMwEQYDVQQDDAptb3VudGViYW5rMB4XDTIzMDYwNjAy 5 | MjE0MVoXDTMzMDYwMzAyMjE0MVowVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlRY 6 | MQ8wDQYDVQQHDAZEYWxsYXMxEzARBgNVBAoMCm1vdW50ZWJhbmsxEzARBgNVBAMM 7 | Cm1vdW50ZWJhbmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM+JWj 8 | JFwtRE++8pTUs6GyWFLKMGSsZDUyJG0n9QivhJPE3fovecKzBdRZZUrAjHWP1Xxp 9 | DLsqfy3FKzD9bEI2bIaa6Bv3QCUiSEheXteBlljltd4Rhup9yWnpwkBmDW4FIxV6 10 | Hm5aOyRx41EGcq4Ir0rwpTMkHRrakCKkyVCuhcPbAeTtJn2jw9QIJB3G2IsWuziU 11 | Fmg2TO90HPUpeoVVEgoO9B9DuCX3QmFBki3xTmm9HnvZ7C0tRZM6Y2aEWoPlzosE 12 | 7PcFjDmd9jmSXo3NED9+tDc4Xaoc9iPBpLMCqe6DaZBbqHxSFFS6spBJClqev09g 13 | 45PC3HqXl/Qxzj7VAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAH2pBSp0wtEmRiuY 14 | 9jhnB8C7pFDOUSUHIfOXKovH92ErqA0KGFvAhCShV71OW63C+7BfJTkt0Gq0pwfD 15 | TdDQ1T9YNFU6NbNbMfSG9u0TMwajnawnNdO5Gk6LtXJFseWsn5PblCVrWD/qvLMF 16 | DcllKYW8a8GBbttngt0bhGvh+DxPesjkHoeIWed6UCo3+Wdypes/vol3GL/e8Re5 17 | uy3eKSBPR5dx/s6l/ZPDEksYV1ve5jv/lBKmRFKv5bFGiTrd5eUE25P2+n9DNGyY 18 | yNrcLvc1WY/0sqGFmo2ci4ECZVDwx/GhKhdZw7IY8tfLfsKR7sAI6nDHO/GbPYYd 19 | piqTpzQ= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /src/models/https/cert/mb-csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICmjCCAYICAQAwVTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlRYMQ8wDQYDVQQH 3 | DAZEYWxsYXMxEzARBgNVBAoMCm1vdW50ZWJhbmsxEzARBgNVBAMMCm1vdW50ZWJh 4 | bmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM+JWjJFwtRE++8pTU 5 | s6GyWFLKMGSsZDUyJG0n9QivhJPE3fovecKzBdRZZUrAjHWP1XxpDLsqfy3FKzD9 6 | bEI2bIaa6Bv3QCUiSEheXteBlljltd4Rhup9yWnpwkBmDW4FIxV6Hm5aOyRx41EG 7 | cq4Ir0rwpTMkHRrakCKkyVCuhcPbAeTtJn2jw9QIJB3G2IsWuziUFmg2TO90HPUp 8 | eoVVEgoO9B9DuCX3QmFBki3xTmm9HnvZ7C0tRZM6Y2aEWoPlzosE7PcFjDmd9jmS 9 | Xo3NED9+tDc4Xaoc9iPBpLMCqe6DaZBbqHxSFFS6spBJClqev09g45PC3HqXl/Qx 10 | zj7VAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAUiKzIKhw1QfOeJ2HGmyC9cyh 11 | r32YyUWVyKWEIdFedcsZcisGE+95UFeakP18LEnfnz8fMfxJHA/J/2rsLxvwx7UI 12 | naZ9IW+RjSbpZvA3HKoiDubc29sBYX6QGdjMYQpxLzJhW20VpPx29TTVmR2Sg2rM 13 | P5zhUtYi8YqE2P8bRGZTMsm585Y36dzUl4u5JKVNWBH6Z7q+339I0i+Y+EfoWDzb 14 | HXztEH9OcyGbWD2sHJGLaEwYVCm6RmFRtCX9LCBjt2lPfmHwm4L+qSfUPRdsfixh 15 | clLJfcE5i11Ydacp6Mlff3PXkM96Aw4t6CE9fudu1uaAMVoSq9vpmSp0tnrtXA== 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /src/models/https/cert/mb-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAzPiVoyRcLURPvvKU1LOhslhSyjBkrGQ1MiRtJ/UIr4STxN36 3 | L3nCswXUWWVKwIx1j9V8aQy7Kn8txSsw/WxCNmyGmugb90AlIkhIXl7XgZZY5bXe 4 | EYbqfclp6cJAZg1uBSMVeh5uWjskceNRBnKuCK9K8KUzJB0a2pAipMlQroXD2wHk 5 | 7SZ9o8PUCCQdxtiLFrs4lBZoNkzvdBz1KXqFVRIKDvQfQ7gl90JhQZIt8U5pvR57 6 | 2ewtLUWTOmNmhFqD5c6LBOz3BYw5nfY5kl6NzRA/frQ3OF2qHPYjwaSzAqnug2mQ 7 | W6h8UhRUurKQSQpanr9PYOOTwtx6l5f0Mc4+1QIDAQABAoIBAA9toq3N/dY2bx47 8 | WjKMdt5awZiQffNv84Ubss+wJQA5JXpLDxrlul8JUEuOUUsfB3ZVJnEt0STIv+Q4 9 | dQ6OSImaL6OXVwuMW38yG6hm0Sfi7jwULWv6UMo5D+zVf01vM1nVozc29S17iCm+ 10 | Z4nptenXb/efJ7NPMYdEFCd9M8J/EpvYEMis0/Jtwvlvg7GO1g6fV0BR/HttJM/x 11 | XBAl9W6LAkL+8/XRJ7iYcCzU6WwUFeCdU/R6kFGngYuMw4r90u6ukWQzuVKXJH7E 12 | mi4o16UKNzKfnKOyLBbpeJFGF5DhtwoJpHAZVS9bVZQIr6p4ugbvCBuRshlLGDdN 13 | v9aJfwECgYEA5ZtmxgTgRIHYhLtURDhJo9113C2vPgkbUIxFfKoZPRWkotEYR0M3 14 | ypgU3mDKn1KMXsrTUq3Uv4+Zvwbym2L3AJCvnyR9FpZoz5U98lLtDDxMSzr9++PA 15 | dO7QfznQ0L9JDSl8Bav40quuFgwdGdcwvWSFpT5Zzb/fTw+ZfrqfxhUCgYEA5Ig5 16 | eRAmbk/cmjoNZB0jp8ijJHlvr5QEnXK0cOT9dQ8xk/moNcn0DkSmpaJK/rb3f0+/ 17 | HhOfMnGfABn+7h7g8rnFmwkd9WIxNsQb64+e33NLzdF//MfU5xLE4oHGCTztx8eZ 18 | ck8RoGB4JvJYcwLt4R/OtL4G6Rfx1JX0pKzthcECgYA2Uyhj3a96RgaGkRQE+BRk 19 | UveZ2q1Fzj3KNwYR0uUZ0M8dPr+xzLOcmZMGcnw+afeQTgjl3P8jO8Syr+Ai561t 20 | Us5apvV5rKirxLHdbcVsSa/7dL+3I1Hb2M037OP9H+UW2iPf66p5nekYilEwVfvQ 21 | M8JzMGdrCOS6/gPhOiKnaQKBgQC9Mcni3+vxB0yqocTUTQtnrELjv2UnBnOLpZqc 22 | m/b5Ikr5JoaLgVX7Ofp8xY8wsGjVjT+7tqLlMAtiGiNjH007pXBimXmj3FbB8Djt 23 | G0l71Ae9rOM4cndflbpJiwZYP4jbC/ONHsiI7VSLabawAIzPA3YtS+SMtLYQONUA 24 | P+mkAQKBgQDRdeTKPPOTF5Md3gdsN4WVdrOkOro+5lCtN9lglRcfrESShV1KtEjz 25 | WznF3OLO/XFpI30itei1HCZjo+nXSUvsV5t6zBOzQ9tzw2/hgaoH6nooZgdT9r1O 26 | 34YsJhUoDmukCEii8IgRRaZyqS1829xqF8Fw5fgYBr9KgA+FHjpSDw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/models/https/httpsServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'), 4 | fs = require('fs'), 5 | https = require('https'), 6 | baseHttpServer = require('../http/baseHttpServer.js'); 7 | 8 | /** 9 | * Represents an https imposter 10 | * @module 11 | */ 12 | 13 | function createBaseServer (options) { 14 | const metadata = { 15 | key: options.key || fs.readFileSync(path.join(__dirname, '/cert/mb-key.pem'), 'utf8'), 16 | cert: options.cert || fs.readFileSync(path.join(__dirname, '/cert/mb-cert.pem'), 'utf8'), 17 | mutualAuth: Boolean(options.mutualAuth), 18 | rejectUnauthorized: options.rejectUnauthorized || false, 19 | ca: options.ca || null 20 | }, 21 | // client certs will not reject the request. It does set the request.client.authorized variable 22 | // to false for all self-signed certs; use rejectUnauthorized: true and a ca: field set to an array 23 | // containing the client cert to see request.client.authorized = true 24 | config = { 25 | key: metadata.key, 26 | cert: metadata.cert, 27 | mutualAuth: metadata.mutualAuth, 28 | rejectUnauthorized: metadata.rejectUnauthorized, 29 | ca: metadata.ca, 30 | requestCert: metadata.mutualAuth && metadata.rejectUnauthorized 31 | }, 32 | createNodeServer = () => https.createServer(config); 33 | 34 | if (options.ciphers) { 35 | metadata.ciphers = options.ciphers.toUpperCase(); 36 | config.ciphers = options.ciphers.toUpperCase(); 37 | } 38 | 39 | return { metadata, createNodeServer }; 40 | } 41 | 42 | module.exports = baseHttpServer(createBaseServer); 43 | -------------------------------------------------------------------------------- /src/models/https/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = JSON.parse(process.argv[2]), 4 | httpsServer = require('./httpsServer.js'), 5 | httpProxy = require('../http/httpProxy.js'), 6 | mbConnection = require('../mbConnection.js').create(config); 7 | 8 | httpsServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => { 9 | mbConnection.setPort(server.port); 10 | mbConnection.setProxy(httpProxy.create(mbConnection.logger())); 11 | 12 | const metadata = server.metadata; 13 | metadata.port = server.port; 14 | console.log(JSON.stringify(metadata)); 15 | }).catch(error => { 16 | console.error(JSON.stringify(error)); 17 | process.exit(1); // eslint-disable-line no-process-exit 18 | }); 19 | -------------------------------------------------------------------------------- /src/models/impostersRepository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'), 4 | path = require('path'), 5 | fileSystemBackedImpostersRepository = require('./filesystemBackedImpostersRepository.js'), 6 | inMemoryImpostersRepository = require('./inMemoryImpostersRepository.js'); 7 | 8 | /** 9 | * An factory abstraction for loading imposters 10 | * @module 11 | */ 12 | 13 | /** 14 | * Creates the repository based on startup configuration 15 | * @param {Object} config - The startup configuration 16 | * @param {Object} logger - The logger 17 | * @returns {Object} - the repository 18 | */ 19 | function create (config, logger) { 20 | if (config.impostersRepository) { 21 | const filename = path.resolve(path.relative(process.cwd(), config.impostersRepository)); 22 | 23 | if (fs.existsSync(filename)) { 24 | try { 25 | return require(filename).create(config, logger); 26 | } 27 | catch (e) { 28 | logger.error(`An error occured while creating custom impostersRepository:\n ${e}`); 29 | return {}; 30 | } 31 | } 32 | else { 33 | logger.warn(`Imposters Respository ${filename} does not exist. The default will be used`); 34 | return this.inMemory(); 35 | } 36 | } 37 | else if (config.datadir) { 38 | return fileSystemBackedImpostersRepository.create(config, logger); 39 | } 40 | else { 41 | return this.inMemory(); 42 | } 43 | } 44 | 45 | function inMemory () { 46 | return inMemoryImpostersRepository.create(); 47 | } 48 | 49 | module.exports = { create, inMemory }; 50 | -------------------------------------------------------------------------------- /src/models/jsonpath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsonPathPlus = require('jsonpath-plus'), 4 | helpers = require('../util/helpers.js'); 5 | 6 | const { JSONPath } = jsonPathPlus; 7 | 8 | /** 9 | * Shared logic for xpath selector 10 | * @module 11 | */ 12 | 13 | /** 14 | * Returns xpath value(s) from given xml 15 | * @param {String} selector - The xpath selector 16 | * @param {String} possibleJSON - the JSON string 17 | * @param {Logger} logger - Optional, used to log JSON parsing errors 18 | * @returns {Object} 19 | */ 20 | function select (selector, possibleJSON, logger) { 21 | const isObject = helpers.isObject; 22 | 23 | try { 24 | const json = isObject(possibleJSON) ? possibleJSON : JSON.parse(possibleJSON), 25 | result = JSONPath(selector, json); 26 | if (typeof result === 'string') { 27 | return result; 28 | } 29 | else if (result.length === 0) { 30 | return undefined; 31 | } 32 | else { 33 | return result; 34 | } 35 | } 36 | catch (e) { 37 | if (logger) { 38 | logger.warn(`Cannot parse as JSON: ${JSON.stringify(possibleJSON)}`); 39 | } 40 | return undefined; 41 | } 42 | } 43 | 44 | module.exports = { select }; 45 | -------------------------------------------------------------------------------- /src/models/smtp/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = JSON.parse(process.argv[2]), 4 | smtpServer = require('./smtpServer'), 5 | mbConnection = require('../mbConnection').create(config); 6 | 7 | smtpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => { 8 | mbConnection.setPort(server.port); 9 | 10 | const metadata = server.metadata; 11 | metadata.port = server.port; 12 | console.log(JSON.stringify(metadata)); 13 | }).catch(error => { 14 | console.error(JSON.stringify(error)); 15 | process.exit(1); // eslint-disable-line no-process-exit 16 | }); 17 | -------------------------------------------------------------------------------- /src/models/smtp/smtpRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Transforms an SMTP request into the simplified API-friendly mountebank request 5 | * @module 6 | */ 7 | 8 | const mailParser = require('mailparser'); 9 | 10 | function addressValues (addresses) { 11 | // mailparser sometimes returns an array, sometimes an object, so we have to normalize 12 | if (!addresses) { 13 | addresses = []; 14 | } 15 | if (!Array.isArray(addresses)) { 16 | addresses = [addresses]; 17 | } 18 | return addresses.map(address => address.value[0]); 19 | } 20 | 21 | function transform (session, email) { 22 | return { 23 | requestFrom: session.remoteAddress, 24 | ip: session.remoteAddress, 25 | envelopeFrom: session.envelope.mailFrom.address, 26 | envelopeTo: session.envelope.rcptTo.map(value => value.address), 27 | from: email.from.value[0], 28 | to: addressValues(email.to), 29 | cc: addressValues(email.cc), 30 | bcc: addressValues(email.bcc), 31 | subject: email.subject, 32 | priority: email.priority || 'normal', 33 | references: email.references || [], 34 | inReplyTo: email.inReplyTo || [], 35 | text: (email.text || '').trim(), 36 | html: (email.html || '').trim(), 37 | attachments: email.attachments || [] 38 | }; 39 | } 40 | 41 | /** 42 | * Transforms the raw SMTP request into the mountebank request 43 | * @param {Object} request - The raw SMTP request 44 | * @returns {Object} 45 | */ 46 | function createFrom (request) { 47 | return new Promise((resolve, reject) => { 48 | const simpleParser = mailParser.simpleParser; 49 | simpleParser(request.source, (err, mail) => { 50 | if (err) { 51 | reject(err); 52 | } 53 | else { 54 | resolve(transform(request.session, mail)); 55 | } 56 | }); 57 | }); 58 | } 59 | 60 | module.exports = { createFrom }; 61 | -------------------------------------------------------------------------------- /src/models/tcp/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = JSON.parse(process.argv[2]), 4 | tcpServer = require('./tcpServer.js'), 5 | tcpProxy = require('./tcpProxy.js'), 6 | mbConnection = require('../mbConnection.js').create(config); 7 | 8 | tcpServer.create(config, mbConnection.logger(), mbConnection.getResponse).then(server => { 9 | mbConnection.setPort(server.port); 10 | mbConnection.setProxy(tcpProxy.create(mbConnection.logger(), server.encoding, server.isEndOfRequest)); 11 | 12 | const metadata = server.metadata; 13 | metadata.port = server.port; 14 | console.log(JSON.stringify(metadata)); 15 | }).catch(error => { 16 | console.error(JSON.stringify(error)); 17 | process.exit(1); // eslint-disable-line no-process-exit 18 | }); 19 | -------------------------------------------------------------------------------- /src/models/tcp/tcpRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const helpers = require('../../util/helpers.js'); 4 | 5 | /** 6 | * Transforms a raw tcp request into the API-friendly representation of one 7 | * @module 8 | */ 9 | 10 | /** 11 | * Transforms the raw tcp request into a mountebank tcp request 12 | * @param {Object} request - The raw tcp request 13 | * @returns {Object} - A promise resolving to the mountebank tcp request 14 | */ 15 | function createFrom (request) { 16 | return Promise.resolve({ 17 | requestFrom: helpers.socketName(request.socket), 18 | ip: request.socket.remoteAddress, 19 | data: request.data 20 | }); 21 | } 22 | 23 | module.exports = { createFrom }; 24 | -------------------------------------------------------------------------------- /src/models/tcp/tcpValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exceptions = require('../../util/errors.js'); 4 | 5 | /** 6 | * Additional tcp-specific validations 7 | * @module 8 | */ 9 | 10 | function validate (request) { 11 | const errors = []; 12 | 13 | if (request.mode && ['text', 'binary'].indexOf(request.mode) < 0) { 14 | errors.push(exceptions.ValidationError("'mode' must be one of ['text', 'binary']")); 15 | } 16 | return errors; 17 | } 18 | 19 | module.exports = { validate }; 20 | -------------------------------------------------------------------------------- /src/public/images/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/arrow_down.png -------------------------------------------------------------------------------- /src/public/images/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/arrow_up.png -------------------------------------------------------------------------------- /src/public/images/book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/book.jpg -------------------------------------------------------------------------------- /src/public/images/dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/dataflow.png -------------------------------------------------------------------------------- /src/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/favicon.ico -------------------------------------------------------------------------------- /src/public/images/forkme_right_orange_ff7600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/forkme_right_orange_ff7600.png -------------------------------------------------------------------------------- /src/public/images/mountebank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/mountebank.png -------------------------------------------------------------------------------- /src/public/images/overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/overview.gif -------------------------------------------------------------------------------- /src/public/images/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/quote.png -------------------------------------------------------------------------------- /src/public/images/tw-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbyars/mountebank/0e8e80db0a620cdfdeb45f43cf7e252ead4582c3/src/public/images/tw-logo.png -------------------------------------------------------------------------------- /src/public/scripts/urlHashHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = module || {}; 4 | 5 | const toggleExpandedOnSection = element => { 6 | $(element).siblings('section').toggleClass('expanded'); 7 | }; 8 | 9 | const addSectionClickHandler = () => { 10 | $('.section-toggler').on('click', event => { 11 | toggleExpandedOnSection(event.currentTarget); 12 | }); 13 | }; 14 | 15 | const hashLocationHandler = window => { 16 | const hashLocation = window.location.hash; 17 | if (hashLocation) { 18 | const $section = $(hashLocation); 19 | if ($section.length > 0) { 20 | $section.trigger('click'); 21 | $(window).scrollTop($section.parent().offset().top); 22 | } 23 | } 24 | }; 25 | 26 | $(document).ready(addSectionClickHandler); 27 | $(document).ready(() => { 28 | hashLocationHandler(window); 29 | }); 30 | 31 | module.exports = { toggleExpandedOnSection, hashLocationHandler }; 32 | -------------------------------------------------------------------------------- /src/public/stylesheets/ie.css: -------------------------------------------------------------------------------- 1 | input[type=search] { 2 | position: absolute; 3 | left: 30em; 4 | top: -5em; 5 | background-color: #fffff0; 6 | } 7 | 8 | #searchButton { 9 | display: inline; 10 | position: absolute; 11 | left: 45.5em; 12 | top: -5em; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/public/stylesheets/imposters.css: -------------------------------------------------------------------------------- 1 | #imposters { 2 | table-layout: fixed; 3 | } 4 | 5 | .inspect-icon:before { 6 | content: "\f002"; 7 | font-family: FontAwesome; 8 | font-weight: bold; 9 | font-style: normal; 10 | } 11 | 12 | .delete-icon:before { 13 | content: "\f057"; 14 | font-family: FontAwesome; 15 | font-weight: bold; 16 | font-style: normal; 17 | color: red; 18 | } 19 | 20 | .add-icon { 21 | float: left; 22 | } 23 | 24 | .add-icon:before { 25 | content: "\f055"; 26 | font-family: FontAwesome; 27 | font-weight: bold; 28 | font-style: normal; 29 | color: green; 30 | } 31 | 32 | .ui-widget-overlay { 33 | background: inherit; 34 | background-color: #111111; 35 | } 36 | 37 | .ui-dialog-titlebar { 38 | padding: 0; 39 | margin: 0; 40 | border: 0; 41 | } 42 | 43 | .ui-widget-header { 44 | background: inherit; 45 | border: 1px solid #aaaaaa; 46 | } 47 | 48 | .ui-dialog-title { 49 | font-family: 'Berkshire Swash'; 50 | } 51 | 52 | ui-dialog { 53 | padding-top: 0; 54 | } 55 | 56 | .ui-button { 57 | background: inherit; 58 | background-color: inherit; 59 | } 60 | 61 | .ui-button-icon-only { 62 | margin-left: inherit; 63 | } 64 | 65 | .ui-widget { 66 | font-family: inherit; 67 | font-size: inherit; 68 | background-color: #f3f2ee; 69 | } 70 | 71 | .ui-widget-content { 72 | background: inherit; 73 | background-color: #f3f2ee; 74 | } 75 | 76 | .ui-dialog button { 77 | margin: 0; 78 | font-family: inherit; 79 | } 80 | 81 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { 82 | font-family: inherit; 83 | font-size: inherit; 84 | } 85 | 86 | select:focus, input:focus { 87 | background-color: #fff5ee; 88 | } 89 | 90 | label { 91 | display: inline-block; 92 | clear: left; 93 | width: 5em; 94 | margin: 0; 95 | text-align: right; 96 | } 97 | 98 | label:after { 99 | content: ":"; 100 | } 101 | 102 | input, select { 103 | display: inline-block; 104 | margin-left: 1em; 105 | } 106 | 107 | input + span, select + span { 108 | display: block; 109 | clear: both; 110 | color: #808080; 111 | font-size: smaller; 112 | margin-left: 7.4em; 113 | } 114 | 115 | #stubs input + span, #stubs select + span { 116 | margin-left: 0; 117 | } 118 | 119 | .template { 120 | display: none; 121 | } 122 | -------------------------------------------------------------------------------- /src/util/combinators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Helpful combinators 5 | * For the non-pedants, a combinator is basically just a function with no free variables. 6 | * For the non-pedants, "no free variables" means that the combinator does not have dependencies 7 | * on things outside the function (e.g. it only depends on the function parameters). 8 | * A strict definition of combinators requires functions as input parameters, but I loosen that here. 9 | * That definition really only serves mathematical modeling of state in pure functional terms 10 | * @module 11 | */ 12 | 13 | /** 14 | * Curries a function parameters, which is to say that it returns a function with reduced arity. 15 | * @example 16 | * function sum (x, y) { return x + y; } 17 | * curry(sum, 1)(2); // returns 3 18 | * curry(sum, 1, 2)(); // returns 3 19 | * @param {Function} fn - The function to curry 20 | * @param {...*} args - The arguments to curry 21 | * @returns {Function} 22 | */ 23 | function curry (fn) { 24 | const args = Array.prototype.slice.call(arguments, 1); 25 | return function () { 26 | const nextArgs = Array.prototype.slice.call(arguments), 27 | allArgs = args.concat(nextArgs); 28 | 29 | return fn.apply(null, allArgs); 30 | }; 31 | } 32 | 33 | /** 34 | * Composes two or more functions 35 | * @example 36 | * function increment (i) { return i + 1; } 37 | * function double (i) { return i * 2; } 38 | * function triple (i) { return i * 3; } 39 | * combinators.compose(increment, double, triple)(1); // returns 7 40 | * @param {...Function} args - The functions to compose 41 | * @returns {Function} A single function that represents the composition of the functions provided 42 | */ 43 | function compose () { 44 | const args = Array.prototype.slice.call(arguments).reverse(); 45 | return obj => args.reduce((result, F) => F(result), obj); 46 | } 47 | 48 | module.exports = { 49 | /** 50 | * Returns what was passed in unchanged, occasionally useful as the default transformation function 51 | * to avoid special case logic 52 | * @param {Object} i - The input 53 | * @returns {Object} Exactly what was passed in 54 | */ 55 | identity: i => i, 56 | /** 57 | * Ignores its parameters, and instead always returns a constant value 58 | * @param {Object} k - The constant to return 59 | * @returns {Function} - A function that will always return the constant 60 | */ 61 | constant: k => () => k, 62 | /** 63 | * A function that does nothing, occasionally useful to avoid special case logic 64 | */ 65 | noop: () => {}, 66 | compose, 67 | curry 68 | }; 69 | -------------------------------------------------------------------------------- /src/util/date.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @module */ 4 | 5 | function toEpochWithoutTime (text) { 6 | // be sure to exclude time so we get accurate text 7 | const dateTextWithoutTime = new Date(Date.parse(text)).toDateString(); 8 | return Date.parse(dateTextWithoutTime); 9 | } 10 | 11 | function sameMonth (firstEpoch, secondEpoch) { 12 | const first = new Date(firstEpoch), 13 | second = new Date(secondEpoch); 14 | 15 | return first.getFullYear() === second.getFullYear() && first.getMonth() === second.getMonth(); 16 | } 17 | 18 | /** 19 | * Translates the distance between two dates within a month of each other to human readable text 20 | * @param {string} thenText - The start date 21 | * @param {string} testNowText - Ignore, used for testing purposes only. 22 | * @returns {string} 23 | */ 24 | function howLongAgo (thenText, testNowText) { 25 | const nowText = testNowText ? testNowText : new Date(Date.now()).toISOString(), // testNow is just for testing purposes 26 | then = toEpochWithoutTime(thenText), 27 | now = toEpochWithoutTime(nowText), 28 | millisecondsInDay = 24 * 60 * 60 * 1000, 29 | daysAgo = Math.floor((now - then) / millisecondsInDay); 30 | 31 | if (daysAgo === 0) { 32 | return 'today'; 33 | } 34 | else if (daysAgo === 1) { 35 | return 'yesterday'; 36 | } 37 | else if (daysAgo < 7) { 38 | return 'this week'; 39 | } 40 | else if (daysAgo < 14) { 41 | return 'last week'; 42 | } 43 | else if (sameMonth(then, now)) { 44 | return 'this month'; 45 | } 46 | else { 47 | return ''; 48 | } 49 | } 50 | 51 | module.exports = { howLongAgo }; 52 | -------------------------------------------------------------------------------- /src/util/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Error types returned by the API 5 | * @module 6 | */ 7 | const inherit = require('./inherit.js'), 8 | helpers = require('./helpers.js'); 9 | 10 | function createError (code, message, options) { 11 | const result = inherit.from(Error, { code, message }); 12 | 13 | if (options) { 14 | Object.keys(options).forEach(key => { 15 | result[key] = options[key]; 16 | }); 17 | } 18 | return result; 19 | } 20 | 21 | function create (code) { 22 | return (message, options) => createError(code, message, options); 23 | } 24 | 25 | function createWithMessage (code, message) { 26 | return options => createError(code, message, options); 27 | } 28 | 29 | // Produces a JSON.stringify-able Error object 30 | // (because message is on the prototype, it doesn't show by default) 31 | function details (error) { 32 | const prototypeProperties = {}; 33 | 34 | ['message', 'name', 'stack'].forEach(key => { 35 | if (error[key]) { 36 | prototypeProperties[key] = error[key]; 37 | } 38 | }); 39 | return helpers.merge(error, prototypeProperties); 40 | } 41 | 42 | module.exports = { 43 | ValidationError: create('bad data'), 44 | InjectionError: create('invalid injection'), 45 | ResourceConflictError: create('resource conflict'), 46 | InsufficientAccessError: createWithMessage('insufficient access', 'Run mb in superuser mode if you want access'), 47 | InvalidProxyError: create('invalid proxy'), 48 | MissingResourceError: create('no such resource'), 49 | InvalidJSONError: createWithMessage('invalid JSON', 'Unable to parse body as JSON'), 50 | CommunicationError: createWithMessage('communication', 'Error communicating with mountebank'), 51 | ProtocolError: create('cannot start server'), 52 | DatabaseError: create('corrupted database'), 53 | UnauthorizedError: createWithMessage('unauthorized', 'If you set the apiKey option, make sure you are sending the correct apiKey in the x-api-key header.'), 54 | details 55 | }; 56 | -------------------------------------------------------------------------------- /src/util/inherit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @module */ 4 | 5 | /** 6 | * Crockford-style prototypical inheritance, which basically allows me to completely 7 | * avoid the new and this operators, which I have an unnatural aversion to 8 | * @param {Object} proto - the object to inherit from 9 | * @param {Object} [obj] - properties to merge into the newly created object as own properties 10 | * @returns {Object} 11 | */ 12 | function from (proto, obj) { 13 | // allow either inherit.from(EventEmitter) or inherit.from({key: 'value'}) 14 | if (typeof proto === 'function') { 15 | proto = new proto(); 16 | } 17 | 18 | obj = obj || {}; 19 | function F () {} 20 | F.prototype = proto; 21 | const result = new F(); 22 | Object.keys(obj).forEach(key => { 23 | result[key] = obj[key]; 24 | }); 25 | return result; 26 | } 27 | 28 | module.exports = { from }; 29 | -------------------------------------------------------------------------------- /src/util/ip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | 5 | function getLocalIPs () { 6 | const interfaces = os.networkInterfaces(), 7 | result = []; 8 | 9 | Object.keys(interfaces).forEach(name => { 10 | interfaces[name].forEach(ip => { 11 | if (ip.internal) { 12 | result.push(ip.address); 13 | if (ip.family === 'IPv4') { 14 | // Prefix for IPv4 address mapped to a compliant IPv6 scheme 15 | result.push(`::ffff:${ip.address}`); 16 | } 17 | } 18 | }); 19 | }); 20 | return result; 21 | } 22 | 23 | function ipWithoutZoneId (ip) { 24 | return ip.replace(/%\w+/, '').toLowerCase(); 25 | } 26 | 27 | function createIPVerification (options) { 28 | const allowedIPs = getLocalIPs(); 29 | 30 | if (!options.localOnly) { 31 | options.ipWhitelist.forEach(ip => { allowedIPs.push(ip.toLowerCase()); }); 32 | } 33 | 34 | if (allowedIPs.indexOf('*') >= 0) { 35 | return () => true; 36 | } 37 | else { 38 | return (ip, logger) => { 39 | if (typeof ip === 'undefined') { 40 | logger.error('Blocking request because no IP address provided. This is likely a bug in the protocol implementation.'); 41 | return false; 42 | } 43 | else { 44 | const allowed = allowedIPs.some(allowedIP => allowedIP === ipWithoutZoneId(ip)); 45 | if (!allowed) { 46 | logger.warn(`Blocking incoming connection from ${ip}. Turn off --localOnly or add to --ipWhitelist to allow`); 47 | } 48 | return allowed; 49 | } 50 | }; 51 | } 52 | } 53 | 54 | module.exports = { createIPVerification }; 55 | -------------------------------------------------------------------------------- /src/util/scopedLogger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @module */ 4 | 5 | const inherit = require('./inherit.js'), 6 | util = require('util'); 7 | 8 | function wrap (wrappedLogger, logger) { 9 | ['debug', 'info', 'warn', 'error'].forEach(level => { 10 | wrappedLogger[level] = function () { 11 | const args = Array.prototype.slice.call(arguments); 12 | args[0] = wrappedLogger.scopePrefix + args[0]; 13 | 14 | // Format here rather than use winston's splat formatter 15 | // to get rid of inconsistent "meta" log elements 16 | const message = util.format.apply(null, args); 17 | logger[level](message); 18 | }; 19 | }); 20 | wrappedLogger.baseLogger = logger; 21 | } 22 | 23 | /** 24 | * Returns a logger that prefixes each message of the given logger with a given scope 25 | * @param {Object} logger - The logger to add a scope to 26 | * @param {string} scope - The prefix for all log messages 27 | * @returns {Object} 28 | */ 29 | function create (logger, scope) { 30 | function formatScope (scopeText) { 31 | return scopeText.indexOf('[') === 0 ? scopeText : `[${scopeText}] `; 32 | } 33 | 34 | const wrappedLogger = inherit.from(logger, { 35 | scopePrefix: formatScope(scope), 36 | withScope: nestedScopePrefix => create(logger, `${wrappedLogger.scopePrefix}${nestedScopePrefix} `), 37 | changeScope: newScope => { 38 | wrappedLogger.scopePrefix = formatScope(newScope); 39 | wrap(wrappedLogger, logger); 40 | } 41 | }); 42 | 43 | wrap(wrappedLogger, logger); 44 | return wrappedLogger; 45 | } 46 | 47 | module.exports = { create }; 48 | -------------------------------------------------------------------------------- /src/views/_footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/_imposter.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= imposter.name || imposter.protocol + ':' + imposter.port %> 4 | 5 | <%= imposter.protocol %> 6 | <%= imposter.port %> 7 | <%= imposter.numberOfRequests %> 8 | 9 | -------------------------------------------------------------------------------- /src/views/config.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'configuration' 3 | description = 'The version, command line parameters, and process information about this running mb process' 4 | %> 5 | 6 | <%- include('_header') -%> 7 | 8 | <% 9 | function isJSONObject (value) { 10 | return typeof value === 'object' && !Array.isArray(value); 11 | } 12 | 13 | function prettyPrint (value) { 14 | return isJSONObject(value) ? JSON.stringify(value, null, 2) : value; 15 | } 16 | %> 17 | 18 |

Config

19 | 20 | 21 | 22 | 23 | 24 | 25 | <% Object.keys(options).forEach(key =>{ %> 26 | 27 | 28 | 30 | 31 | <% }); -%> 32 |
version<%= version %>
<%= key %><% if (isJSONObject(options[key])) { -%>
<% } -%><%= prettyPrint(options[key]) %><% if (isJSONObject(options[key])) { -%>
<% } -%> 29 |
33 | 34 |

Process Information

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
node version<%= process.nodeVersion %>
architecture<%= process.architecture %>
platform<%= process.platform %>
rss<%= process.rss %>
heapTotal<%= process.heapTotal %>
heapUsed<%= process.heapUsed %>
uptime<%= process.uptime %>
cwd<%= process.cwd %>
70 | 71 | <%- include('_footer') -%> 72 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/addStub-description.ejs: -------------------------------------------------------------------------------- 1 |
2 |

The array index to add the stub. Stubs are always evaluated in array order, and the first stub 3 | whose predicates match the request will be used. If you leave this value off the request, the stub 4 | will be added to the end of the array.

5 |
6 | 7 |
8 |

The stub to add. See the imposter contract for more information.

9 |

More information: imposter contract

10 |
11 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/addStub.ejs: -------------------------------------------------------------------------------- 1 |
{
 2 | <%- indent(2) %>"index": 1,
 3 | <%- indent(2) %>"stub": {
 4 |     "responses": [{
 5 |       "is": {
 6 |         "body": "Hello, world!"
 7 |       }
 8 |     }]
 9 |   }
10 | }
11 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/config-description.ejs: -------------------------------------------------------------------------------- 1 |
2 |

The mountebank version

3 |
4 | 5 |
6 |

The command line options used to start mb.

7 |

More information: command line options

8 |
9 | 10 |
11 |

Information about the running mb process

12 |
13 | 14 |
15 |

The version of node.js

16 |
17 | 18 |
19 |

The operating system and architecture of the machine running mountebank.

20 |
21 | 22 |
23 |

The memory (in bytes) used by mountebank, and heap usage of the V8 JavaScript engine.

24 |
25 | 26 |
27 |

The number of seconds this process has been running.

28 |
29 | 30 |
31 |

The current directory, used to start mb

32 |
33 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/config.ejs: -------------------------------------------------------------------------------- 1 |
{
 2 | <%- indent(2) %>"version": "1.4.1",
 3 | <%- indent(2) %>"options": {
 4 |     "port": 2525,
 5 |     "pidfile": "mb.pid",
 6 |     "logfile": "mb.log",
 7 |     "loglevel": "info",
 8 |     "configfile": "",
 9 |     "allowInjection": false,
10 |     "mock": true,
11 |     "debug": true
12 |   },
13 | <%- indent(2) %>"process": {
14 | <%- indent(4) %>"nodeVersion": "v6.9.1",
15 | <%- indent(4) %>"architecture": "x64",
16 |     "platform": "darwin",
17 | <%- indent(4) %>"rss": 29822976,
18 |     "heapTotal": 18635008,
19 |     "heapUsed": 9294352,
20 | <%- indent(4) %>"uptime": 16,
21 | <%- indent(4) %>"cwd": "/Users/bbyars/src/mountebank"
22 |   }
23 | }
24 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/home-description.ejs: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/home.ejs: -------------------------------------------------------------------------------- 1 |
{
 2 | <%- indent(2) %>"_links": {
 3 | <%- indent(4) %>"imposters": {
 4 |       "href": "http://localhost:2525/imposters"
 5 |     },
 6 | <%- indent(4) %>"config": {
 7 |       "href": "http://localhost:2525/config"
 8 |     },
 9 | <%- indent(4) %>"logs": {
10 |       "href": "http://localhost:2525/logs"
11 |     }
12 |   }
13 | }
14 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/imposters-description.ejs: -------------------------------------------------------------------------------- 1 |
2 |

An array of imposter objects.

3 |

4 | 5 |
6 |

A single imposter object.

7 | 8 |

By default, the fields shown are the only ones returned. Use 9 | additional query parameters to return 10 | the full imposter definition, and optionally remove proxies for subsequent replays

11 | 12 |

More information: imposter contract

13 |
14 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/imposters.ejs: -------------------------------------------------------------------------------- 1 |
{
 2 | <%- indent(2) %>"imposters": [
 3 | <%- indent(4) %>{
 4 |       "protocol": "http",
 5 |       "port": 4546,
 6 |       "_links": {
 7 |         "self": {
 8 |           "href": "http://localhost:2525/imposters/4546"
 9 |         }
10 |       }
11 |     }
12 |   ]
13 | }
14 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/logs-description.ejs: -------------------------------------------------------------------------------- 1 |
2 |

An array of all log statements captured in the current log file.

3 |
4 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/logs.ejs: -------------------------------------------------------------------------------- 1 |
{
 2 | <%- indent(2) %>"logs": [
 3 |     {
 4 |       "level": "info",
 5 |       "message": "[mb:2525] mountebank v1.4.1 (node v4.2.0) now taking orders - point your browser to http://localhost:2525 for help",
 6 |       "timestamp": "2015-10-20T02:24:41.818Z"
 7 |     },
 8 |     {
 9 |       "level": "info",
10 |       "message": "[mb:2525] Adios - see you soon?",
11 |       "timestamp": "2015-10-20T02:31:38.109Z"
12 |     }
13 |   ]
14 | }
15 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/stub-description.ejs: -------------------------------------------------------------------------------- 1 |
2 |

The stub to add. See the imposter contract for more information.

3 |

More information: imposter contract

4 |
5 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/stub.ejs: -------------------------------------------------------------------------------- 1 |
{
2 | <%- indent(2) %>"responses": [{
3 |     "is": {
4 |       "body": "Hello, world!"
5 |     }
6 |   }]
7 | }
8 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/stubs-description.ejs: -------------------------------------------------------------------------------- 1 |
2 |

The stub to add. See the imposter contract for more information.

3 |

More information: imposter contract

4 |
5 | -------------------------------------------------------------------------------- /src/views/docs/api/contracts/stubs.ejs: -------------------------------------------------------------------------------- 1 |
{
 2 |   "stubs": [
 3 | <%- indent(4) %>{
 4 |       "responses": [{
 5 |         "is": {
 6 |           "body": "Hello, world!"
 7 |         }
 8 |       }]
 9 |     }
10 |   ]
11 | }
12 | -------------------------------------------------------------------------------- /src/views/docs/api/errors.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'errors' 3 | description = 'Information about standard errors returned by mountebank' 4 | %> 5 | 6 | <%- include('../../_header') -%> 7 | 8 |

Errors

9 | 10 |

mountebank is sorry you are running into errors. To help you diagnose 11 | the problem, he has listed the API error codes below. Feel free to 12 | ask for help if you get stuck.

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 |
Error codeDescription
bad dataA general validation error. Something in the input is invalid.
invalid injectionYou are using the inject operator, either as a predicate 26 | or response type. Perhaps you have not run mb with the 27 | --allowInjection flag, or perhaps there is an error in the 28 | JavaScript you are injecting. Consider using the node.js 29 | console or mountebank logger objects to help 30 | debug the problem.
resource conflictYou have specified a port already in use by another application. 35 | Try killing the other application, or use a different port for the 36 | imposter.
insufficient accessYou have requested an imposter port that the user running 41 | mb is not authorized to provide. Either request a 42 | different port, or run mb as sudo.
invalid proxyYou have configured a proxy response type with an invalid address, 47 | or the server has refused the connection. Double check the DNS name or 48 | IP address, and double check the port. If mb is running 49 | on a different network than you are, does he have the same connectivity 50 | to the proxy that you do?
no such resourceA 404 error code. Probably, you are attempting to access an 55 | imposter that has not been created yet. Try POSTing to /imposters 56 | first?
invalid JSONYou have sent a request body, but mountebank is unable to parse it.
63 | 64 | <%- include('../../_footer') -%> 65 | -------------------------------------------------------------------------------- /src/views/docs/api/fault/connectionReset.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
POST /imposters HTTP/1.1
 4 | Host: localhost:<%= port %>
 5 | Accept: application/json
 6 | Content-Type: application/json
 7 | 
 8 | {
 9 |   "port": 4554,
10 |   "protocol": "tcp",
11 |   "mode": "text",
12 |   "stubs": [
13 |     {
14 |       "responses": [
15 |         {
16 |           "fault": "CONNECTION_RESET_BY_PEER"
17 |         }
18 |       ]
19 |     }
20 |   ]
21 | }
22 |
23 | 24 | 25 | 26 | 27 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/views/docs/api/fault/randomDataThenClose.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
POST /imposters HTTP/1.1
 4 | Host: localhost:<%= port %>
 5 | Accept: application/json
 6 | Content-Type: application/json
 7 | 
 8 | {
 9 |   "port": 4554,
10 |   "protocol": "tcp",
11 |   "mode": "text",
12 |   "stubs": [
13 |     {
14 |       "responses": [
15 |         {
16 |           "fault": "RANDOM_DATA_THEN_CLOSE"
17 |         }
18 |       ]
19 |     }
20 |   ]
21 | }
22 |
23 | 24 | 25 | 26 | 27 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /src/views/docs/api/faults.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'fault simulation' 3 | description = 'Using mountebank to simulate network faults' 4 | %> 5 | 6 | <%- include('../../_header') -%> 7 | 8 |

Fault Simulation

9 | 10 |

Fault simulation allows us to check how our application behaves when downstream dependencies don't respond as expected due to network failures. 11 | Mountebank already has the ability to specify delays via "wait" but we may also want to test when the connection is abruptly reset or garbage data is returned, similar to some of Wiremock's fault simulation functionality 12 |

13 | 14 |

Fault simulation has only been implemented for http, https and tcp protocols. 15 | The "fault" response type is mutually exclusive with respect to the other response types as it necessarily prevents any normal response being returned to the client.

16 | 17 |

The fault response type takes a single parameter specifying which fault to simulate.

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
ParameterDescription
CONNECTION_RESET_BY_PEERClose the connection.
RANDOM_DATA_THEN_CLOSESend garbage then close the connection.
33 | 34 |

Select the behavior of the fault below for a relevant example:

35 | 36 |
37 |
38 | 40 | Connection Reset by Peer 41 | 42 |
43 | <%- include('fault/connectionReset') -%> 44 |
45 |
46 |
47 | 49 | Random Data then Close 50 | 51 |
52 | <%- include('fault/randomDataThenClose') -%> 53 |
54 |
55 |
56 | 57 | <%- include('../../_footer') -%> 58 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/and.ejs: -------------------------------------------------------------------------------- 1 |

The and predicate matches if all of its sub-predicates match.

2 | 3 | 4 | 5 |
POST /imposters HTTP/1.1
 6 | Host: localhost:<%= port %>
 7 | Accept: application/json
 8 | Content-Type: application/json
 9 | 
10 | {
11 |   "port": 4554,
12 |   "protocol": "tcp",
13 |   "mode": "text",
14 |   "stubs": [
15 |     {
16 |       "responses": [{ "is": { "data": "matches" } }],
17 |       "predicates": [
18 |         {
19 |           "and": [
20 |             { "startsWith": { "data": "start" } },
21 |             { "endsWith": { "data": "end\n" } },
22 |             { "contains": { "data": "middle" } }
23 |           ]
24 |         }
25 |       ]
26 |     }
27 |   ]
28 | }
29 |
30 | 31 |

The first request matches all sub-predicates, triggering the stub response:

32 | 33 | 34 |
echo 'start middle end' | nc localhost 4554
35 | 36 | 37 |
matches
38 |
39 |
40 | 41 |

The stub matches two of the three sub-predicates, which is not enough to match 42 | the and predicate.

43 | 44 |

The stub does not match none of the sub-predicates match...

45 | 46 | 47 |
echo 'start end' | nc localhost 4554
48 | 49 |

No response is sent.

50 | 51 | 52 |

53 | 
54 |
55 |
56 | 57 | 58 | 61 | 62 |
63 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/contains.ejs: -------------------------------------------------------------------------------- 1 |

Let's create a binary TCP imposter with multiple stubs:

2 | 3 | 4 | 5 |
POST /imposters HTTP/1.1
 6 | Host: localhost:<%= port %>
 7 | Accept: application/json
 8 | Content-Type: application/json
 9 | 
10 | {
11 |   "port": 4547,
12 |   "protocol": "tcp",
13 |   "mode": "binary",
14 |   "stubs": [
15 |     {
16 |       "responses": [{ "is": { "data": "Zmlyc3QgcmVzcG9uc2U=" } }],
17 |       "predicates": [{ "contains": { "data": "AgM=" } }]
18 |     },
19 |     {
20 |       "responses": [{ "is": { "data": "c2Vjb25kIHJlc3BvbnNl" } }],
21 |       "predicates": [{ "contains": { "data": "Bwg=" } }]
22 |     },
23 |     {
24 |       "responses": [{ "is": { "data": "dGhpcmQgcmVzcG9uc2U=" } }],
25 |       "predicates": [{ "contains": { "data": "Bwg=" } }]
26 |     }
27 |   ]
28 | }
29 |
30 | 31 |

We're sending a base64-encoded version of four bytes: 0x1, 0x2, 0x3, 32 | and 0x4. Our first predicate is a base64 encoded version of 0x2 and 0x3. 33 | The response is a base64-encoded version of the string "first response":

34 | 35 | 36 |
echo 'AQIDBA==' | base64 --decode | nc localhost 4547
37 | 38 | 39 |
first response
40 |
41 |
42 | 43 |

Next we'll send 0x5, 0x6, 0x7, and 0x8, matching on a predicate 44 | encoding 0x7 and 0x8:

45 | 46 | 47 |
echo 'BQYHCA==' | base64 --decode | nc localhost 4547
48 | 49 | 50 |
second response
51 |
52 |
53 | 54 |

The third stub will never run, since it matches the same requests as the 55 | second stub. mountebank always chooses the first stub that matches based on 56 | the order you add them to the stubs array when creating the 57 | imposter.

58 | 59 | 60 | 63 | 64 |
65 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/endsWith.ejs: -------------------------------------------------------------------------------- 1 |

Let's create a binary-based imposter with multiple stubs:

2 | 3 | 4 | 5 |
POST /imposters HTTP/1.1
 6 | Host: localhost:<%= port %>
 7 | Accept: application/json
 8 | Content-Type: application/json
 9 | 
10 | {
11 |   "port": 4549,
12 |   "protocol": "tcp",
13 |   "mode": "binary",
14 |   "stubs": [
15 |     {
16 |       "responses": [{ "is": { "data": "Zmlyc3QgcmVzcG9uc2U=" } }],
17 |       "predicates": [{ "endsWith": { "data": "AwQ=" } }]
18 |     },
19 |     {
20 |       "responses": [{ "is": { "data": "c2Vjb25kIHJlc3BvbnNl" } }],
21 |       "predicates": [{ "endsWith": { "data": "BQY=" } }]
22 |     },
23 |     {
24 |       "responses": [{ "is": { "data": "dGhpcmQgcmVzcG9uc2U=" } }],
25 |       "predicates": [{ "endsWith": { "data": "BQY=" } }]
26 |     }
27 |   ]
28 | }
29 |
30 | 31 |

We'll use the command line base64 tool to decode the 32 | request to binary before sending to the imposter. We're sending a 33 | base64-encoded version of four bytes: 0x1, 0x2, 0x3, and 0x4. Our 34 | first predicate is a base64 encoded version of 0x3 and 0x4. The 35 | response is a base64-encoded version of the string "first response":

36 | 37 | 38 |
echo 'AQIDBA==' | base64 --decode | nc localhost 4549
39 | 40 | 41 |
first response
42 |
43 |
44 | 45 |

Next we'll send 0x1, 0x2, 0x4, 0x5, and 0x6, matching on a predicate 46 | encoding 0x5 and 0x6:

47 | 48 | 49 |
echo 'AQIDBAUG' | base64 --decode | nc localhost 4549
50 | 51 | 52 |
second response
53 |
54 |
55 | 56 |

The third stub will never run, since it matches the same requests as the 57 | second stub. mountebank always chooses the first stub that matches based on 58 | the order you add them to the stubs array when creating the 59 | imposter.

60 | 61 | 62 | 65 | 66 |
67 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/inject.ejs: -------------------------------------------------------------------------------- 1 |

The inject predicate allows you to inject JavaScript to determine if 2 | the predicate should match or not. The JavaScript should be a function that accepts 3 | the request object (and optionally a logger) and returns true or false. 4 | See the injection page for details.

5 | 6 |

The execution will have access to a node.js environment. The following example uses 7 | node's Buffer object to decode base64 to a byte array.

8 | 9 | 10 | 11 |
POST /imposters HTTP/1.1
12 | Host: localhost:<%= port %>
13 | Accept: application/json
14 | Content-Type: application/json
15 | 
16 | {
17 |   "port": 4555,
18 |   "protocol": "tcp",
19 |   "mode": "binary",
20 |   "stubs": [
21 |     {
22 |       "responses": [{ "is": { "data": "Zmlyc3QgcmVzcG9uc2U=" } }],
23 |       "predicates": [{
24 |         "inject": "function (request, logger) { logger.info('Inside injection'); return Buffer.from(request.data, 'base64')[2] > 100; }"
25 |       }]
26 |     },
27 |     {
28 |       "responses": [{ "is": { "data": "c2Vjb25kIHJlc3BvbnNl" } }],
29 |       "predicates": [{
30 |         "inject": "request => { return Buffer.from(request.data, 'base64')[2] <= 100; }"
31 |       }]
32 |     }
33 |   ]
34 | }
35 |
36 | 37 |

The first stub matches if the third byte is greater than 100. The request we're 38 | sending is an encoding [99, 100, 101]:

39 | 40 | 41 |
echo 'Y2Rl' | base64 --decode | nc localhost 4555
42 | 43 | 44 |
first response
45 |
46 |
47 | 48 |

The logs will also show the injected log output. The second predicate has to 49 | match a request originating from localhost with the third byte less than or equal 50 | to 100. We're sending [98, 99, 100]:

51 | 52 | 53 |
echo 'YmNk' | base64 --decode | nc localhost 4555
54 | 55 |

...giving the response:

56 | 57 | 58 |
second response
59 |
60 |
61 | 62 | 63 | 66 | 67 |
68 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/matches.ejs: -------------------------------------------------------------------------------- 1 |

Let's create a text-based imposter with multiple stubs. Binary imposters 2 | cannot use the matches predicate.

3 | 4 | 5 | 6 |
POST /imposters HTTP/1.1
 7 | Host: localhost:<%= port %>
 8 | Accept: application/json
 9 | Content-Type: application/json
10 | 
11 | {
12 |   "port": 4550,
13 |   "protocol": "tcp",
14 |   "mode": "text",
15 |   "stubs": [
16 |     {
17 |       "responses": [{ "is": { "data": "first response" } }],
18 |       "predicates": [{
19 |         "matches": { "data": "^first\\Wsecond" },
20 |         "caseSensitive": true
21 |       }]
22 |     },
23 |     {
24 |       "responses": [{ "is": { "data": "second response" } }],
25 |       "predicates": [{ "matches": { "data": "second\\s+request" } }]
26 |     },
27 |     {
28 |       "responses": [{ "is": { "data": "third response" } }],
29 |       "predicates": [{ "matches": { "data": "second\\s+request" } }]
30 |     }
31 |   ]
32 | }
33 |
34 | 35 |

The first stub requires a case-sensitive match on a string starting with 36 | "first", followed by a non-word character, followed by "second":

37 | 38 | 39 |
echo 'first second' | nc localhost 4550
40 | 41 | 42 |
first response
43 |
44 |
45 | 46 |

The second stub is not case-sensitive.

47 | 48 | 49 |
echo 'Second Request' | nc localhost 4550
50 | 51 | 52 |
second response
53 |
54 |
55 | 56 |

The third stub will never run, since it matches the same requests as the 57 | second stub. mountebank always chooses the first stub that matches based on 58 | the order you add them to the stubs array when creating the 59 | imposter.

60 | 61 | 62 | 65 | 66 |
67 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/not.ejs: -------------------------------------------------------------------------------- 1 |

The not predicate negates its child predicate.

2 | 3 | 4 | 5 |
POST /imposters HTTP/1.1
 6 | Host: localhost:<%= port %>
 7 | Accept: application/json
 8 | Content-Type: application/json
 9 | 
10 | {
11 |   "port": 4552,
12 |   "protocol": "tcp",
13 |   "mode": "text",
14 |   "stubs": [
15 |     {
16 |       "responses": [{ "is": { "data": "not test" } }],
17 |       "predicates": [{ "not": { "equals": { "data": "test\n" } } }]
18 |     },
19 |     {
20 |       "responses": [{ "is": { "data": "test" } }],
21 |       "predicates": [{ "equals": { "data": "test\n" } }]
22 |     }
23 |   ]
24 | }
25 |
26 | 27 |

The first stub matches if the is sub-predicate does not match:

28 | 29 | 30 |
echo 'production' | nc localhost 4552
31 | 32 | 33 |
not test
34 |
35 |
36 | 37 |

As expected, the second stub matches if the is sub-predicate does match:

38 | 39 | 40 |
echo 'test' | nc localhost 4552
41 | 42 | 43 |
test
44 |
45 |
46 | 47 | 48 | 51 | 52 |
53 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/or.ejs: -------------------------------------------------------------------------------- 1 |

The or predicate matches if any of its sub-predicates match.

2 | 3 | 4 | 5 |
POST /imposters HTTP/1.1
 6 | Host: localhost:<%= port %>
 7 | Accept: application/json
 8 | Content-Type: application/json
 9 | 
10 | {
11 |   "port": 4553,
12 |   "protocol": "tcp",
13 |   "mode": "text",
14 |   "stubs": [
15 |     {
16 |       "responses": [{ "is": { "data": "matches" } }],
17 |       "predicates": [
18 |         {
19 |           "or": [
20 |             { "startsWith": { "data": "start" } },
21 |             { "endsWith": { "data": "end\n" } },
22 |             { "contains": { "data": "middle" } }
23 |           ]
24 |         }
25 |       ]
26 |     }
27 |   ]
28 | }
29 |
30 | 31 |

The stub matches if the first sub-predicate matches:

32 | 33 | 34 |
echo 'start data transmission' | nc localhost 4553
35 | 36 | 37 |
matches
38 |
39 |
40 | 41 |

The stub matches if the second sub-predicate matches:

42 | 43 | 44 |
echo 'data transmission end' | nc localhost 4553
45 | 46 | 47 |
matches
48 |
49 |
50 | 51 |

The stub matches if the last sub-predicate matches:

52 | 53 | 54 |
echo 'data middle transmission' | nc localhost 4553
55 | 56 | 57 |
matches
58 |
59 |
60 | 61 |

The stub does not match none of the sub-predicates match...

62 | 63 | 64 |
echo 'data transmission' | nc localhost 4553
65 | 66 |

...which yields an empty response:

67 | 68 | 69 |

70 | 
71 |
72 |
73 | 74 | 75 | 78 | 79 |
80 | -------------------------------------------------------------------------------- /src/views/docs/api/predicates/startsWith.ejs: -------------------------------------------------------------------------------- 1 |

Let's create a text-based imposter with multiple stubs. Binary imposters won't 2 | see any interesting behavior difference with only startsWith predicates:

3 | 4 | 5 | 6 |
POST /imposters HTTP/1.1
 7 | Host: localhost:<%= port %>
 8 | Accept: application/json
 9 | Content-Type: application/json
10 | 
11 | {
12 |   "port": 4548,
13 |   "protocol": "tcp",
14 |   "mode": "text",
15 |   "stubs": [
16 |     {
17 |       "responses": [{ "is": { "data": "first response" } }],
18 |       "predicates": [{ "startsWith": { "data": "first" } }]
19 |     },
20 |     {
21 |       "responses": [{ "is": { "data": "second response" } }],
22 |       "predicates": [{ "startsWith": { "data": "second" } }]
23 |     },
24 |     {
25 |       "responses": [{ "is": { "data": "third response" } }],
26 |       "predicates": [{ "startsWith": { "data": "second" } }]
27 |     }
28 |   ]
29 | }
30 |
31 | 32 |

The match is not case-sensitive:

33 | 34 | 35 |
echo 'FIRST REQUEST' | nc localhost 4548
36 | 37 | 38 |
first response
39 |
40 |
41 | 42 |

The same is true for the second stub.

43 | 44 | 45 |
echo 'Second Request' | nc localhost 4548
46 | 47 | 48 |
second response
49 |
50 |
51 | 52 |

The third stub will never run, since it matches the same requests as the 53 | second stub. mountebank always chooses the first stub that matches based on 54 | the order you add them to the stubs array when creating the 55 | imposter.

56 | 57 | 58 | 61 | 62 |
63 | -------------------------------------------------------------------------------- /src/views/docs/cli/customFormatters.ejs: -------------------------------------------------------------------------------- 1 |

A formatter is a CommonJS module that exports two functions: load, used 2 | to load the configuration when the configfile option is passed to mb start, 3 | and save, used to save the configuration for mb save. The formatter 4 | gives you total control over how the test data is stored, allowing you to improve readability 5 | (which suffers by default from JSON's single line requirement for strings) or to convert between 6 | formats of other service virtualization tools.

7 | 8 |

The default formatter that ships with mountebank is 9 | described above ("Default config file parsing"). The example below shows a simple (and silly) formatter 10 | that encodes all data as Base64.

11 | 12 |
'use strict';
13 | 
14 | function encode (obj) {
15 |     return Buffer.from(JSON.stringify(obj)).toString('base64');
16 | }
17 | 
18 | function decode (text) {
19 |     return Buffer.from(text, 'base64').toString('utf8');
20 | }
21 | 
22 | function load (options) {
23 |     const fs = require('fs'),
24 |         contents = fs.readFileSync(options.configfile, { encoding: 'utf8' });
25 |     return JSON.parse(decode(contents));
26 | }
27 | 
28 | function save (options, imposters) {
29 |     const fs = require('fs');
30 | 
31 |     if (options.customName && imposters.imposters.length > 0) {
32 |         imposters.imposters[0].name = options.customName;
33 |     }
34 |     fs.writeFileSync(options.savefile, encode(imposters));
35 | }
36 | 
37 | module.exports = { load, save };
38 | 39 |

All CLI options are passed to both the load and save functions, allowing 40 | you to define custom options specific to your formatter. You can see an example of this in the 41 | save function, which takes a customName option and makes it the saved 42 | name of the first imposter.

43 | 44 |

The code above shows a synchronous implementation, but mountebank will accept 45 | a promise return value.

46 | 47 |

Assuming the module is saved as 'customFormatter.js', the following commands will use it 48 | for saving and loading:

49 | 50 |
mb save --savefile mb.json --formatter customFormatter
51 | mb restart --configfile mb.json --formatter customFormatter
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/docs/cli/help.ejs: -------------------------------------------------------------------------------- 1 |
mb help
2 | 3 |

Lists basic command line help. You can get more detailed help for a command 4 | with the following:

5 | 6 |
mb command --help
7 | -------------------------------------------------------------------------------- /src/views/docs/cli/replay.ejs: -------------------------------------------------------------------------------- 1 |
mb replay [options]
2 | 3 |

The replay command is a convenience that removes all proxies, 4 | effectively switching from record mode to replay mode. Assuming 5 | mountebank is running on port 3000, you would run the following command:

6 | 7 |
mb replay --port 3000
8 | 9 |

That will reset the imposter configuration by sending a PUT command to /imposters 10 | based on the current configuration, excluding the proxies (using the 11 | ?removeProxies=true query parameter). The following options are available:

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
OptionDescriptionDefault
--port 2525The port of the running the mountebank server2525
--host mbserver.localThe hostname of the running mountebank serverlocalhost
--rcfile .mbrcThe run commands file containing startup configuration (a JSON-equivalent representation 32 | of the command line arguments). When the same option is listed 33 | in both the rcfile and the command line, the command line option takes 34 | precedence.N/A
--helpShow help for the commandN/A
43 | -------------------------------------------------------------------------------- /src/views/docs/cli/restart.ejs: -------------------------------------------------------------------------------- 1 |
mb restart [options]
2 | 3 |

The restart command is equivalent to running mb stop followed 4 | by mb start. To ensure a clean stop, the --pidfile 5 | must match the one passed to the original mb start process. By default, 6 | this means you must run restart in the same working directory you originally 7 | ran the start command from.

8 | 9 |

The restart command has all the same command line options as the 10 | start command.

11 | -------------------------------------------------------------------------------- /src/views/docs/cli/stop.ejs: -------------------------------------------------------------------------------- 1 |
mb stop [options]
2 | 3 |

Stops mb, shutting down all open sockets. Generally, mb stop 4 | must be run from the same directory as the mb start command, although 5 | that is configurable with the pidfile option:

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
OptionDescriptionDefault
--pidfileThe path to the pidfile, which stores the process id of the running mb process. 16 | Must match the pidfile passed in the mb start command.mb.pid
--rcfile .mbrcThe run commands file containing startup configuration (a JSON-equivalent representation 22 | of the command line arguments). When the same option is listed 23 | in both the rcfile and the command line, the command line option takes 24 | precedence.N/A
--helpShow help for the commandN/A
33 | -------------------------------------------------------------------------------- /src/views/docs/mentalModel.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'mental model' 3 | description = 'A mental model of how mountebank works' 4 | %> 5 | 6 | <%- include('../_header') -%> 7 | 8 |

Mental model

9 | 10 |

Manning Publishing has been kind enough to help diagram 11 | the following mental model, describing how mountebank works:

12 | 13 | 14 | mental model 16 | 17 | 18 |

Lexicon

19 | 20 |
21 |
imposter
22 |
A server representing a test double. An imposter is identified by a port and 23 | a protocol. mountebank is non-modal and can create as many imposters as your test requires.
24 | 25 |
stub
26 |
A set of configuration used to generate a response for an imposter. An imposter 27 | can have 0 or more stubs, each of which are associated with different predicates.
28 | 29 |
predicate
30 |
A condition that determines whether a given stub is responsible for responding. Each 31 | stub can have 0 or more predicates.
32 | 33 |
response
34 |
The configuration that generates the response for a stub. Each stub can have 35 | 0 or more responses.
36 | 37 |
response type
38 |
Defines the specific type of configuration used to generate a response. The simplest type is called 39 | is, and allows you to define the imposter's response directly. mountebank also 40 | supports a proxy response type, which allows record-replay behavior, and an inject 41 | response type, which allows you to script mountebank responses. Each response has exactly 42 | one response type.
43 | 44 |
stub behavior
45 |
Adds additional post-processing to a response, for example by adding latency to the 46 | response or augmenting the response with more information. A response can have zero 47 | or more behaviors, which represent a pipeline of such transformations.
48 | 49 |
50 | 51 | <%- include('../_footer') -%> 52 | -------------------------------------------------------------------------------- /src/views/docs/security.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'security' 3 | description = 'Securing mountebank against remote execution attacks' 4 | %> 5 | 6 | <%- include('../_header') -%> 7 | 8 |

Security

9 | 10 |

mountebank is programmable through injection. This makes 11 | the tool very extensible and flexible, but it should only be used with an understanding of the 12 | security implications. When you enable the --allowInjection 13 | flag, you aren't just giving yourself the ability to extend mountebank: you're also potentially enabling 14 | attackers remote execution capabilities on your machine.

15 | 16 |

mountebank highly recommends you take the following approaches to securing your environment if you 17 | require --allowInjection:

18 | 19 | 30 | 31 |

The most secure option, of course, is to simply not use the --allowInjection flag. 32 | If there are common operations you find yourself using injection for, feel free to suggest those operations 33 | as core features in a future release of mountebank.

34 | 35 |

By default, CORS is disabled to prevent CSRF attacks. To enable, you must explicitly pass safe origins 36 | on the command line using the --origin flag.

37 | 38 | <%- include('../_footer') -%> 39 | -------------------------------------------------------------------------------- /src/views/feed.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mountebank releases 6 | Release notes for mountebank 7 | 8 | <% if (hasNextPage) { %> 9 | 10 | <% } %> 11 | 12 | http://<%= host %>/feed 13 | <%= releases[0].date %>T00:00:00Z 14 | 15 | Brandon Byars 16 | brandon.byars@gmail.com 17 | http://<%= host %> 18 | 19 | 20 | <% releases.forEach(release => { %> 21 | 22 | mountebank <%= release.version %> release 23 | http://<%= host %>/releases/<%= release.version %> 24 | <%= release.date %>T00:00:00Z 25 | 26 | 28 | ]]> 29 | 30 | 31 | <% }); %> 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/views/imposter.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'imposter information' 3 | description = 'Information about a specific mountebank imposter' 4 | %> 5 | 6 | <%- include('_header') -%> 7 | 8 |

<%= imposter.protocol %> Imposter on port <%= imposter.port %>

9 | 10 |

Requests

11 | 12 | <% imposter.requests.forEach(request => { -%> 13 |
<%= JSON.stringify(request, null, 2) %>
14 | <% }); -%> 15 | 16 |

Stubs

17 | 18 | <% imposter.stubs.forEach(stub => { -%> 19 |
<%= JSON.stringify(stub, null, 2) %>
20 | <% }); -%> 21 | 22 | <%- include('_footer') -%> 23 | -------------------------------------------------------------------------------- /src/views/imposters.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'running imposters' 3 | description = 'Information about the currently running imposters' 4 | %> 5 | 6 | <%- include('_header') -%> 7 | 8 | 9 | 10 |

Imposters

11 | 12 |

You may explore other UIs, including:

13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <% imposters.forEach(imposter => { %> 28 | <%- include('_imposter', {imposter}) -%> 29 | <% }); -%> 30 |
nameprotocolport# requests
31 | 32 | 33 | <%- include('_footer') -%> 34 | -------------------------------------------------------------------------------- /src/views/license.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'license' 3 | description = 'mountebank uses the MIT license' 4 | %> 5 | 6 | <%- include('_header') -%> 7 | 8 |

The MIT License (MIT)

9 | 10 |

Copyright © 2013 mountebank

11 | 12 |

Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions:

18 | 19 |

The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software.

21 | 22 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE.

29 | 30 | <%- include('_footer') -%> 31 | -------------------------------------------------------------------------------- /src/views/logs.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'logs' 3 | description = 'Logs of the currently running mb process' 4 | 5 | function formatLogEntry (logEntry) { 6 | return '' + logEntry.level + ': ' + escape(logEntry.message) + ''; 7 | } 8 | %> 9 | 10 | <%- include('_header') -%> 11 | 12 |

Logs

13 | 14 |
Follow log
15 | 16 |

17 | <% logs.forEach(function (logEntry) { -%>
18 | <%- formatLogEntry(logEntry) %>
19 | <% }) -%>
20 | 
21 | 22 | 76 | 77 | <%- include('_footer') -%> 78 | -------------------------------------------------------------------------------- /src/views/releases.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'releases' 3 | description = 'All past releases of mountebank, saved for posterity' 4 | %> 5 | 6 | <%- include('_header') -%> 7 | 8 |

Releases

9 | 10 |

Below are the releases starting with the most recent. Subscribe to the 11 | ATOM feed to stay up to date with future releases.

12 | 13 | 14 | 15 | 16 | 17 | 18 | <% releases.forEach(release => { %> 19 | 20 | 21 | 22 | 23 | <% }); %> 24 |
VersionRelease Date
<%= release.version %><%= release.date %>
25 | 26 | <%- include('_footer') -%> 27 | -------------------------------------------------------------------------------- /src/views/releases/v2.5.0.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

This release is largely centered around modernizing the codebase and simplifying the build process. 7 | That involved removing some previously supported install options:

8 | 9 |

Install changes

10 | 18 | 19 |

New Features

20 | 24 | 25 |

Bug Fixes

26 | 33 | 34 |

Contributors

35 |

Many thanks to the following kind folks for help with this release, either through bug reports, 36 | suggestions, or direct code contributions:

37 | 38 | 47 | 48 |

Install

49 | 50 |
npm install -g mountebank@<%= releaseVersion %>
51 | 52 | -------------------------------------------------------------------------------- /src/views/releases/v2.6.0.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

New Features

7 | 14 | 15 |

Bug Fixes

16 | 20 | 21 |

Contributors

22 |

Many thanks to the following kind folks for help with this release, either through bug reports, 23 | suggestions, or direct code contributions:

24 | 25 | 32 | 33 |

Install

34 | 35 |
npm install -g mountebank@<%= releaseVersion %>
36 | -------------------------------------------------------------------------------- /src/views/releases/v2.7.0.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

New Features

7 | 11 | 12 |

Bug Fixes

13 | 18 | 19 |

Contributors

20 |

Many thanks to the following kind folks for help with this release, either through bug reports, 21 | suggestions, or direct code contributions:

22 | 23 | 29 | 30 |

Install

31 | 32 |
npm install -g mountebank@<%= releaseVersion %>
33 | -------------------------------------------------------------------------------- /src/views/releases/v2.8.0.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

New Features

7 | 12 | 13 |

Bug Fixes

14 | 21 | 22 |

Contributors

23 |

Many thanks to the following kind folks for help with this release, either through bug reports, 24 | suggestions, or direct code contributions:

25 | 26 | 33 | 34 |

Install

35 | 36 |
npm install -g mountebank@<%= releaseVersion %>
37 | -------------------------------------------------------------------------------- /src/views/releases/v2.8.1.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

This release served only to fix a deployment issue. Please see 7 | v2.8.0 for release notes

8 | -------------------------------------------------------------------------------- /src/views/releases/v2.8.2.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

New Features

7 | 10 | 11 |

Bug Fixes

12 | 15 | 16 |

Contributors

17 |

Many thanks to the following kind folks for help with this release, either through bug reports, 18 | suggestions, or direct code contributions:

19 | 20 | 23 | 24 |

Install

25 | 26 |
npm install -g mountebank@<%= releaseVersion %>
27 | -------------------------------------------------------------------------------- /src/views/releases/v2.9.0.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

New Features

7 | 10 | 11 |

Bug Fixes

12 | 16 | 17 |

Contributors

18 |

Many thanks to the following kind folks for help with this release, either through bug reports, 19 | suggestions, or direct code contributions:

20 | 21 | 29 | 30 |

Install

31 | 32 |
npm install -g mountebank@<%= releaseVersion %>
33 | -------------------------------------------------------------------------------- /src/views/releases/v2.9.1.ejs: -------------------------------------------------------------------------------- 1 |

v<%= releaseVersion %>

2 | 3 |

Be sure to keep up with the latest releases by subscribing to the 4 | ATOM feed.

5 | 6 |

v2.9.1 was released simply to fix a deployment version mismatch of v2.9.0; there are no other functional changes

7 | 8 |

Install

9 | 10 |
npm install -g mountebank@<%= releaseVersion %>
11 | -------------------------------------------------------------------------------- /src/views/sitemap.ejs: -------------------------------------------------------------------------------- 1 | http://www.mbtest.org/ 2 | http://www.mbtest.org/imposters 3 | http://www.mbtest.org/logs 4 | http://www.mbtest.org/config 5 | http://www.mbtest.org/feed 6 | http://www.mbtest.org/support 7 | http://www.mbtest.org/license 8 | http://www.mbtest.org/faqs 9 | http://www.mbtest.org/docs/gettingStarted 10 | http://www.mbtest.org/docs/mentalModel 11 | http://www.mbtest.org/docs/commandLine 12 | http://www.mbtest.org/docs/security 13 | http://www.mbtest.org/docs/communityExtensions 14 | http://www.mbtest.org/docs/api/overview 15 | http://www.mbtest.org/docs/api/contracts 16 | http://www.mbtest.org/docs/api/mocks 17 | http://www.mbtest.org/docs/api/stubs 18 | http://www.mbtest.org/docs/api/predicates 19 | http://www.mbtest.org/docs/api/xpath 20 | http://www.mbtest.org/docs/api/json 21 | http://www.mbtest.org/docs/api/jsonpath 22 | http://www.mbtest.org/docs/api/proxies 23 | http://www.mbtest.org/docs/api/injection 24 | http://www.mbtest.org/docs/api/behaviors 25 | http://www.mbtest.org/docs/api/errors 26 | http://www.mbtest.org/docs/api/faults 27 | http://www.mbtest.org/docs/protocols/http 28 | http://www.mbtest.org/docs/protocols/https 29 | http://www.mbtest.org/docs/protocols/tcp 30 | http://www.mbtest.org/docs/protocols/smtp 31 | http://www.mbtest.org/docs/protocols/custom 32 | http://www.mbtest.org/metrics 33 | http://www.mbtest.org/releases 34 | <% releases.forEach (release => { -%> 35 | http://www.mbtest.org/releases/<%= release.version %> 36 | <% }); -%> 37 | -------------------------------------------------------------------------------- /src/views/support.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | title = 'support' 3 | description = 'Ask questions about anything related to mountebank on our Google Group' 4 | %> 5 | 6 | <%- include('_header') -%> 7 | 8 |

Support

9 | 10 |

mountebank is sorry you're having trouble, but he is eager to help. A 11 | Google group 12 | has been set up to help you. Don't be shy!

13 | 14 | <%- include('_footer') -%> 15 | -------------------------------------------------------------------------------- /tasks/createProtocolsFile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let root = '.'; 4 | const path = require('path'); 5 | 6 | if (process.env.MB_EXECUTABLE) { 7 | root = path.normalize(`${path.dirname(process.env.MB_EXECUTABLE)}/..`); 8 | } 9 | 10 | const protocols = { 11 | smtp: { createCommand: `node ${root}/src/models/smtp/index.js` }, 12 | http: { createCommand: `node ${root}/src/models/http/index.js` }, 13 | https: { createCommand: `node ${root}/src/models/https/index.js` }, 14 | tcp: { createCommand: `node ${root}/src/models/tcp/index.js` } 15 | }, 16 | fs = require('fs'); 17 | 18 | const protocolFile = process.argv[2] || 'protocols.json'; 19 | fs.writeFileSync(protocolFile, JSON.stringify(protocols, null, 2)); 20 | -------------------------------------------------------------------------------- /tasks/deploy/docs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'), 4 | version = require('../../package.json').version, 5 | dir = `docs/mountebank/${version}`, 6 | execSync = require('child_process').execSync; 7 | 8 | if (!fs.existsSync(dir)) { 9 | console.error('No docs exist; run "npm run jsdoc" first'); 10 | process.exit(1); // eslint-disable-line no-process-exit 11 | } 12 | if (!process.env.FIREBASE_TOKEN) { 13 | console.error('FIREBASE_TOKEN environment variable must be set'); 14 | process.exit(1); // eslint-disable-line no-process-exit 15 | } 16 | 17 | console.log('Deploying docs to firebase...'); 18 | fs.copyFileSync('./firebase.json', `${dir}/firebase.json`); 19 | execSync(`../../../node_modules/.bin/firebase deploy --token "${process.env.FIREBASE_TOKEN}" --project firebase-mountebank`, 20 | { cwd: dir, stdio: 'inherit' }); 21 | -------------------------------------------------------------------------------- /tasks/dist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const run = require('./run').run, 4 | fs = require('fs-extra'); 5 | 6 | async function distPackage (source, destination, packageTransformer) { 7 | fs.ensureDirSync(`dist/${destination}`); 8 | const pkg = JSON.parse(fs.readFileSync(`${source}/package.json`)); 9 | 10 | pkg.files.forEach(file => { 11 | fs.copySync(`${source}/${file}`, `dist/${destination}/${file}`); 12 | }); 13 | 14 | packageTransformer(pkg); 15 | fs.writeFileSync(`dist/${destination}/package.json`, JSON.stringify(pkg, null, 2)); 16 | 17 | await run('npm', ['install', '--production'], { cwd: `dist/${destination}` }); 18 | await run('npm', ['pack'], { cwd: `dist/${destination}` }); 19 | } 20 | 21 | async function packageMountebank () { 22 | await distPackage('.', 'mountebank', pkg => { 23 | delete pkg.devDependencies; 24 | Object.keys(pkg.scripts).forEach(script => { 25 | // We don't package most tasks and don't want users running them anyhow 26 | if (['start', 'restart', 'stop'].indexOf(script) < 0) { 27 | delete pkg.scripts[script]; 28 | } 29 | }); 30 | fs.copySync('./Dockerfile', 'dist/mountebank/Dockerfile'); 31 | }); 32 | } 33 | 34 | async function packageMbTest () { 35 | await distPackage('mbTest', 'test', pkg => { 36 | pkg.dependencies.mountebank = 'file:../mountebank'; 37 | const lockfile = JSON.parse(fs.readFileSync('dist/test/package-lock.json')); 38 | lockfile.dependencies.mountebank.version = 'file:../mountebank'; 39 | fs.writeFileSync('dist/test/package-lock.json', JSON.stringify(lockfile, null, 2)); 40 | }); 41 | } 42 | 43 | fs.removeSync('dist'); 44 | packageMountebank() 45 | .then(() => packageMbTest()) 46 | .then(() => console.log('packages available in dist directory')) 47 | .catch(error => { 48 | console.error(error); 49 | process.exit(1); // eslint-disable-line no-process-exit 50 | }); 51 | -------------------------------------------------------------------------------- /tasks/lints/deadCheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const scan = require('./shared/scan'), 4 | fs = require('fs'), 5 | thisPackage = require('../../package.json'), 6 | dependencies = Object.keys(thisPackage.dependencies).concat(Object.keys(thisPackage.devDependencies)), 7 | usedCount = {}, 8 | dependencyCheck = file => { 9 | const contents = fs.readFileSync(file, 'utf8'); 10 | 11 | dependencies.forEach(dependency => { 12 | if (contents.indexOf("require('" + dependency) >= 0 || 13 | contents.indexOf("loadNpmTasks('" + dependency + "')") >= 0) { 14 | usedCount[dependency] += 1; 15 | } 16 | }); 17 | }, 18 | exclusions = [ 19 | 'node_modules', 20 | 'docs', 21 | '.git', 22 | '.DS_Store', 23 | '.idea', 24 | 'images', 25 | 'dist', 26 | 'mountebank.iml', 27 | 'mb.log', 28 | '*.pid', 29 | 'package-lock.json', 30 | '*.csv' 31 | ], 32 | whitelist = [ 33 | 'coveralls', 34 | 'eslint', 35 | 'eslint-plugin-node', 36 | 'eslint-plugin-mocha', 37 | 'firebase-tools', 38 | 'jsdoc', 39 | 'mocha', 40 | 'mocha-multi-reporters', 41 | 'mountebank-formatters', 42 | 'nc', 43 | 'nyc', 44 | 'snyk', 45 | 'mbTest' 46 | ], 47 | errors = []; 48 | 49 | dependencies.forEach(dependency => { usedCount[dependency] = 0; }); 50 | whitelist.forEach(dependency => { usedCount[dependency] += 1; }); 51 | 52 | scan.forEachFileIn('.', dependencyCheck, { exclude: exclusions }); 53 | 54 | dependencies.forEach(dependency => { 55 | if (usedCount[dependency] === 0) { 56 | errors.push(dependency + ' is depended on in package.json but is never required'); 57 | } 58 | }); 59 | 60 | errors.forEach(err => console.error(err)); 61 | process.exit(errors.length); // eslint-disable-line no-process-exit 62 | -------------------------------------------------------------------------------- /tasks/lints/licenseCheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const checker = require('license-checker'), 4 | validLicenses = ['MIT', 'ISC', 'Apache', 'BSD', 'CC0', 'Artistic-2.0', 'CC-BY-3.0', 'Unlicense']; 5 | 6 | checker.init({ 7 | start: '.', 8 | production: true 9 | }, function (err, packages) { 10 | if (err) { 11 | console.error(err); 12 | process.exit(1); // eslint-disable-line no-process-exit 13 | } 14 | else { 15 | const failures = {}; 16 | Object.keys(packages).forEach(name => { 17 | const supported = validLicenses.some(license => { 18 | return packages[name].licenses.indexOf(license) >= 0; 19 | }); 20 | if (!supported) { 21 | failures[name] = packages[name]; 22 | } 23 | }); 24 | if (Object.keys(failures).length > 0) { 25 | console.error('Invalid license(s); either change the dependency or add the license to the valid list: '); 26 | console.error(JSON.stringify(failures, null, 2)); 27 | process.exit(1); // eslint-disable-line no-process-exit 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /tasks/lints/objectCheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const scan = require('./shared/scan'), 4 | fs = require('fs'), 5 | errors = [], 6 | check = function (file) { 7 | const contents = fs.readFileSync(file, 'utf8'); 8 | if (contents.indexOf("=== 'object'") > 0) { 9 | errors.push(`${file} appears to do a typecheck against object without using helpers.isObject`); 10 | } 11 | }, 12 | exclusions = ['node_modules', 'mbTest', 'dist', 'objectCheck.js', 'helpers.js']; 13 | 14 | scan.forEachFileIn('.', check, { exclude: exclusions, filetype: '.js' }); 15 | 16 | errors.forEach(err => console.error(err)); 17 | process.exit(errors.length); // eslint-disable-line no-process-exit 18 | -------------------------------------------------------------------------------- /tasks/lints/shared/scan.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'), 4 | path = require('path'); 5 | 6 | function exclude (exclusions, file) { 7 | return (exclusions || []).some(function (exclusion) { 8 | if (exclusion[0] === '*') { 9 | return path.extname(file) === exclusion.substring(1); 10 | } 11 | else { 12 | return path.basename(file) === exclusion; 13 | } 14 | }); 15 | } 16 | 17 | function include (filetype, file) { 18 | return !filetype || file.indexOf(filetype, file.length - filetype.length) >= 0; 19 | } 20 | 21 | function forEachFileIn (dir, fileCallback, options) { 22 | fs.readdirSync(dir).forEach(file => { 23 | const filePath = path.join(dir, file); 24 | 25 | if (!exclude(options.exclude, filePath)) { 26 | if (fs.lstatSync(filePath).isDirectory()) { 27 | forEachFileIn(filePath, fileCallback, options); 28 | } 29 | else if (include(options.filetype, filePath)) { 30 | fileCallback(filePath); 31 | } 32 | } 33 | }); 34 | } 35 | 36 | module.exports = { forEachFileIn }; 37 | -------------------------------------------------------------------------------- /tasks/mbtest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const spawn = require('child_process').spawn; 4 | 5 | function exec (command, args) { 6 | return new Promise(resolve => { 7 | const process = spawn(command, args, { stdio: 'inherit' }); 8 | process.on('exit', code => resolve(code)); 9 | process.on('error', err => { 10 | console.error(err); 11 | resolve(1); 12 | }); 13 | }); 14 | } 15 | 16 | function mochaParamsFor (testType) { 17 | return [ 18 | 'node_modules/mocha/bin/mocha', 19 | '--forbid-only', 20 | '--reporter', 21 | 'mocha-multi-reporters', 22 | '--reporter-options', 23 | `configFile=mbTest/${testType}/config.json`, 24 | `mbTest/${testType}/**/*.js` 25 | ]; 26 | } 27 | 28 | async function runTests () { 29 | const mbExitCode = await exec('node', ['tasks/mb.js', 'restart', '--allowInjection', '--localOnly']); 30 | if (mbExitCode !== 0) { 31 | console.error('mb failed to start'); 32 | process.exit(mbExitCode); // eslint-disable-line no-process-exit 33 | } 34 | 35 | const testType = process.argv[2]; 36 | const exitCode = await exec('node', mochaParamsFor(testType)); 37 | await exec('node', ['tasks/mb.js', 'stop']); 38 | return exitCode; 39 | } 40 | 41 | runTests().then(code => process.exit(code)); // eslint-disable-line no-process-exit 42 | -------------------------------------------------------------------------------- /tasks/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const spawn = require('child_process').spawn, 4 | os = require('os'); 5 | 6 | function isWindows () { 7 | return os.platform().indexOf('win') === 0; 8 | } 9 | 10 | async function run (command, args, options) { 11 | if (isWindows()) { 12 | // spawn on Windows requires an exe 13 | args.unshift(command); 14 | args.unshift('/c'); 15 | command = 'cmd.exe'; 16 | } 17 | 18 | options.stdio = 'inherit'; 19 | 20 | console.log(`${command} ${args.join(' ')} with ${JSON.stringify(options)}`); 21 | 22 | return new Promise((resolve, reject) => { 23 | const proc = spawn(command, args, options); 24 | 25 | proc.on('close', function (exitCode) { 26 | if (exitCode === 0) { 27 | resolve(); 28 | } 29 | else { 30 | reject(exitCode); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | module.exports = { run }; 37 | -------------------------------------------------------------------------------- /tasks/version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'), 4 | thisPackage = JSON.parse(fs.readFileSync('./package.json')), 5 | buildNumber = process.env.CIRCLE_BUILD_NUM; 6 | 7 | if (typeof buildNumber !== 'undefined') { 8 | thisPackage.version = `${thisPackage.version}-beta.${buildNumber}`; 9 | fs.writeFileSync('./package.json', JSON.stringify(thisPackage, null, 2) + '\n'); 10 | } 11 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "xunit, spec", 3 | "xunitReporterOptions": { 4 | "output": "testResults/unit/results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/controllers/feedControllerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('../../src/controllers/feedController'), 4 | assert = require('assert'), 5 | mock = require('../mock').mock; 6 | 7 | describe('feedController', function () { 8 | describe('#getRelease', function () { 9 | it('should prevent path traversal attacks', function () { 10 | const response = { status: mock().returns({ send: mock() }) }, 11 | releases = [{ version: 'v2.3.0', date: '2020-09-07' }], 12 | controller = Controller.create(releases), 13 | request = { 14 | headers: { host: 'localhost' }, 15 | params: { version: 'v2.3.0%2f..%2f..%2f_header' } 16 | }; 17 | 18 | controller.getRelease(request, response); 19 | 20 | assert.ok(response.status.wasCalledWith(404)); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/controllers/homeControllerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('../../src/controllers/homeController'), 4 | assert = require('assert'), 5 | FakeResponse = require('../fakes/fakeResponse'); 6 | 7 | describe('homeController', function () { 8 | describe('#get', function () { 9 | it('should return base hypermedia', function () { 10 | const response = FakeResponse.create(), 11 | controller = Controller.create([]); 12 | 13 | controller.get({}, response); 14 | 15 | assert.deepEqual(response.body, { 16 | _links: { 17 | imposters: { href: '/imposters' }, 18 | config: { href: '/config' }, 19 | logs: { href: '/logs' } 20 | } 21 | }); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/fakes/fakeImpostersRepository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | function create () { 5 | return { fake: 'fake' }; 6 | } 7 | 8 | module.exports = { 9 | create 10 | }; 11 | -------------------------------------------------------------------------------- /test/fakes/fakeLogger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'), 4 | assert = require('assert'); 5 | 6 | function create () { 7 | const logger = { calls: {} }; 8 | logger.toString = function () { return JSON.stringify(logger.calls, null, 4); }; 9 | 10 | ['debug', 'info', 'warn', 'error'].forEach(function (level) { 11 | logger.calls[level] = []; 12 | logger[level] = function () { 13 | logger.calls[level].push(util.format.apply(logger, arguments)); 14 | }; 15 | logger[level].assertLogged = message => { 16 | assert.ok(logger.calls[level].some(function (entry) { 17 | return entry.indexOf(message) >= 0; 18 | }), JSON.stringify(logger.calls, null, 4)); 19 | }; 20 | }); 21 | 22 | return logger; 23 | } 24 | 25 | module.exports = { 26 | create 27 | }; 28 | -------------------------------------------------------------------------------- /test/fakes/fakeRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function to (href, params) { 4 | const url = new URL(href, 'http://localhost:2525'); 5 | let query = {}; 6 | 7 | if (url.search !== '') { 8 | query = require('querystring').parse(url.search.substr(1)); 9 | } 10 | 11 | return { 12 | url: href, 13 | query: query, 14 | params: params 15 | }; 16 | } 17 | 18 | module.exports = { to }; 19 | -------------------------------------------------------------------------------- /test/fakes/fakeResponse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function create () { 4 | return { 5 | headers: {}, 6 | send: function (body) { this.body = body; }, 7 | setHeader: function (key, value) { this.headers[key] = value; }, 8 | format: function (selectors) { selectors.json(); } 9 | }; 10 | } 11 | 12 | module.exports = { 13 | create 14 | }; 15 | -------------------------------------------------------------------------------- /test/mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function mock () { 4 | let wasCalled = false, 5 | actualArguments = [], 6 | message = '', 7 | slice = Array.prototype.slice, 8 | retVal; 9 | 10 | function setMessage (expected, actual) { 11 | message = `\nExpected call with ${JSON.stringify(expected)}`; 12 | if (wasCalled) { 13 | message += `\nActual called with ${JSON.stringify(actual)}`; 14 | } 15 | else { 16 | message += '\nActually never called'; 17 | } 18 | } 19 | 20 | function stubFunction () { 21 | wasCalled = true; 22 | actualArguments = slice.call(arguments); 23 | return retVal; 24 | } 25 | 26 | stubFunction.returns = function (value) { 27 | retVal = value; 28 | return stubFunction; 29 | }; 30 | 31 | stubFunction.wasCalled = () => wasCalled; 32 | 33 | stubFunction.wasCalledWith = function () { 34 | const expected = slice.call(arguments), 35 | actual = actualArguments.slice(0, expected.length); // allow matching only first few params 36 | setMessage(expected, actualArguments); 37 | 38 | if (JSON.stringify(expected) === '[]') { 39 | console.log('Expected params not captured; please do not convert function to lambda because it loses arguments variable'); 40 | return false; 41 | } 42 | return wasCalled && JSON.stringify(actual) === JSON.stringify(expected); 43 | }; 44 | 45 | stubFunction.message = () => message; 46 | 47 | return stubFunction; 48 | } 49 | 50 | module.exports = { mock }; 51 | -------------------------------------------------------------------------------- /test/models/impostersRepositoryTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | FakeLogger = require('../fakes/fakeLogger'), 5 | impostersRepository = require('../../src/models/impostersRepository'); 6 | 7 | describe('ImpostersRepository', function () { 8 | describe('#create', function () { 9 | describe('custom impostersRepository', function () { 10 | let logger; 11 | 12 | beforeEach(function () { 13 | logger = FakeLogger.create(); 14 | impostersRepository.inMemory = require('../mock').mock(); 15 | }); 16 | 17 | it('should return the custom impostersRepository', function () { 18 | const config = { 19 | impostersRepository: 'test/fakes/fakeImpostersRepository.js' 20 | }; 21 | assert.deepEqual(impostersRepository.create(config, logger), require('../fakes/fakeImpostersRepository.js').create()); 22 | assert.ok(!impostersRepository.inMemory.wasCalled()); 23 | }); 24 | 25 | it('should default to inMemory if the custom impostersRepository does not exist', function () { 26 | const config = { 27 | impostersRepository: '../doesnotexist' 28 | }; 29 | impostersRepository.create(config, logger); 30 | assert.ok(impostersRepository.inMemory.wasCalled()); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/models/predicates/andTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | predicates = require('../../../src/models/predicates'); 5 | 6 | describe('predicates', function () { 7 | describe('#and', function () { 8 | it('should return true if all sub-predicate is true', function () { 9 | const predicate = { and: [{ equals: { field: 'this' } }, { startsWith: { field: 'th' } }] }, 10 | request = { field: 'this' }; 11 | assert.ok(predicates.evaluate(predicate, request)); 12 | }); 13 | 14 | it('should return false if any sub-predicate is false', function () { 15 | const predicate = { and: [{ equals: { field: 'this' } }, { equals: { field: 'that' } }] }, 16 | request = { field: 'this' }; 17 | assert.ok(!predicates.evaluate(predicate, request)); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/models/predicates/notTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | predicates = require('../../../src/models/predicates'); 5 | 6 | describe('predicates', function () { 7 | describe('#not', function () { 8 | it('should return true for non empty request field if exists is true', function () { 9 | const predicate = { not: { equals: { field: 'this' } } }, 10 | request = { field: 'that' }; 11 | assert.ok(predicates.evaluate(predicate, request)); 12 | }); 13 | 14 | it('should return false for empty request field if exists is true', function () { 15 | const predicate = { not: { equals: { field: 'this' } } }, 16 | request = { field: 'this' }; 17 | assert.ok(!predicates.evaluate(predicate, request)); 18 | }); 19 | 20 | it('should throw exception if invalid sub-predicate', function () { 21 | try { 22 | const predicate = { not: { invalid: { field: 'this' } } }, 23 | request = { field: 'this' }; 24 | predicates.evaluate(predicate, request); 25 | assert.fail('should have thrown'); 26 | } 27 | catch (error) { 28 | assert.strictEqual(error.code, 'bad data'); 29 | assert.strictEqual(error.message, 'missing predicate'); 30 | } 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/models/predicates/orTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | predicates = require('../../../src/models/predicates'); 5 | 6 | describe('predicates', function () { 7 | describe('#or', function () { 8 | it('should return true if any sub-predicate is true', function () { 9 | const predicate = { or: [{ equals: { field: 'this' } }, { equals: { field: 'that' } }] }, 10 | request = { field: 'this' }; 11 | assert.ok(predicates.evaluate(predicate, request)); 12 | }); 13 | 14 | it('should return false if no sub-predicate is true', function () { 15 | const predicate = { or: [{ equals: { field: 'this' } }, { equals: { field: 'that' } }] }, 16 | request = { field: 'what' }; 17 | assert.ok(!predicates.evaluate(predicate, request)); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/models/smtp/smtpRequestTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | SmtpRequest = require('../../../src/models/smtp/smtpRequest'); 5 | 6 | describe('smtpRequest', function () { 7 | describe('#createFrom', function () { 8 | it('should parse SMTP data', async function () { 9 | let session = { 10 | remoteAddress: 'RemoteAddress', 11 | envelope: { 12 | mailFrom: { address: 'EnvelopeFrom' }, 13 | rcptTo: [{ address: 'EnvelopeTo' }] 14 | } 15 | }, 16 | stream = new require('stream').Readable(); 17 | stream._read = () => {}; // eslint-disable-line no-underscore-dangle 18 | stream.push('From: From \r\n'); 19 | stream.push('To: To1 \r\n'); 20 | stream.push('To: To2 \r\n'); 21 | stream.push('CC: CC1 \r\n'); 22 | stream.push('CC: CC2 \r\n'); 23 | stream.push('BCC: BCC1 \r\n'); 24 | stream.push('BCC: BCC2 \r\n'); 25 | stream.push('Subject: Subject\r\n'); 26 | stream.push('\r\nBody'); 27 | stream.push(null); 28 | 29 | const smtpRequest = await SmtpRequest.createFrom({ source: stream, session: session }); 30 | 31 | assert.deepEqual(smtpRequest, { 32 | requestFrom: 'RemoteAddress', 33 | envelopeFrom: 'EnvelopeFrom', 34 | envelopeTo: ['EnvelopeTo'], 35 | from: { address: 'from@mb.org', name: 'From' }, 36 | to: [{ address: 'to1@mb.org', name: 'To1' }, { address: 'to2@mb.org', name: 'To2' }], 37 | cc: [{ address: 'cc1@mb.org', name: 'CC1' }, { address: 'cc2@mb.org', name: 'CC2' }], 38 | bcc: [{ address: 'bcc1@mb.org', name: 'BCC1' }, { address: 'bcc2@mb.org', name: 'BCC2' }], 39 | subject: 'Subject', 40 | priority: 'normal', 41 | references: [], 42 | inReplyTo: [], 43 | ip: 'RemoteAddress', 44 | text: 'Body', 45 | html: '', 46 | attachments: [] 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/models/tcp/tcpRequestTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | TcpRequest = require('../../../src/models/tcp/tcpRequest'); 5 | 6 | describe('tcpRequest', function () { 7 | describe('#createFrom', function () { 8 | it('should echo data', async function () { 9 | const request = await TcpRequest.createFrom({ socket: {}, data: 'DATA' }); 10 | 11 | assert.strictEqual(request.data, 'DATA'); 12 | }); 13 | 14 | it('should format requestFrom from socket', async function () { 15 | const socket = { remoteAddress: 'HOST', remotePort: 'PORT' }, 16 | request = await TcpRequest.createFrom({ socket: socket, data: '' }); 17 | 18 | assert.strictEqual(request.requestFrom, 'HOST:PORT'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/models/tcp/tcpValidatorTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | validator = require('../../../src/models/tcp/tcpValidator'); 5 | 6 | describe('tcpValidator', function () { 7 | 8 | describe('#validate', function () { 9 | it('should be valid for missing mode', function () { 10 | assert.deepEqual(validator.validate({}), []); 11 | }); 12 | 13 | it('should be valid for text mode', function () { 14 | assert.deepEqual(validator.validate({ mode: 'text' }), []); 15 | }); 16 | 17 | it('should be valid for binary mode', function () { 18 | assert.deepEqual(validator.validate({ mode: 'binary' }), []); 19 | }); 20 | 21 | it('should not be valid for incorrect mode', function () { 22 | assert.deepEqual(validator.validate({ mode: 'TEXT' }), [{ 23 | code: 'bad data', 24 | message: "'mode' must be one of ['text', 'binary']" 25 | }]); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/public/scripts/urlHashHandlerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | mock = require('../../mock').mock, 5 | JSDOM = require('jsdom').JSDOM, 6 | jquery = require('../../../src/public/scripts/jquery/jquery-3.6.1.min.js'); 7 | 8 | function initJQuery (htmlDocument) { 9 | const window = global.window = new JSDOM(htmlDocument).window; 10 | window.scrollTo = () => {}; // not implemented in jsdom 11.x 11 | global.document = window.document; 12 | global.$ = jquery(window); 13 | return window; 14 | } 15 | 16 | describe('url-hash-handler', function () { 17 | describe('toggleSection', function () { 18 | it('should add class expanded to the section', function () { 19 | const htmlDocument = '
' + 20 | '' + 21 | '
' + 22 | '
'; 23 | initJQuery(htmlDocument); 24 | const toggleSection = require('../../../src/public/scripts/urlHashHandler').toggleExpandedOnSection; 25 | const $sectionToBeChecked = $('#sectionToBeChecked'); 26 | 27 | toggleSection($('#elementToBeClicked')); 28 | assert.equal($sectionToBeChecked.hasClass('expanded'), true); 29 | }); 30 | 31 | it('should remove class expanded from the section', function () { 32 | const htmlDocument = '
' + 33 | '' + 34 | '
' + 35 | '
'; 36 | initJQuery(htmlDocument); 37 | const toggleSection = require('../../../src/public/scripts/urlHashHandler').toggleExpandedOnSection; 38 | const $sectionToBeChecked = $('#sectionToBeChecked'); 39 | 40 | toggleSection($('#elementToBeClicked')); 41 | assert.equal($sectionToBeChecked.hasClass('expanded'), false); 42 | }); 43 | }); 44 | 45 | describe('hashLocationHandler', function () { 46 | it('should click on the section from hash', function () { 47 | const window = initJQuery('
'); 48 | const sectionOnClick = mock(); 49 | $('#sectionToBeTested').on('click', sectionOnClick); 50 | window.location.hash = 'sectionToBeTested'; 51 | const hashLocationHandler = require('../../../src/public/scripts/urlHashHandler').hashLocationHandler; 52 | hashLocationHandler(window); 53 | assert.equal(sectionOnClick.wasCalled(), true); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/util/combinatorsTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | combinators = require('../../src/util/combinators'); 5 | 6 | describe('combinators', function () { 7 | describe('#identity', function () { 8 | it('should return its argument', function () { 9 | assert.strictEqual('arg', combinators.identity('arg')); 10 | }); 11 | }); 12 | 13 | describe('#constant', function () { 14 | it('should return a function that always returns the same thing', function () { 15 | const K = combinators.constant('constant'); 16 | assert.strictEqual('constant', K(0)); 17 | assert.strictEqual('constant', K(1)); 18 | }); 19 | }); 20 | 21 | describe('#noop', function () { 22 | it('does nothing', function () { 23 | const state = { key: 'value' }; 24 | const result = combinators.noop.call(state); 25 | assert.strictEqual(result, undefined); 26 | assert.deepEqual(state, { key: 'value' }); 27 | }); 28 | }); 29 | 30 | describe('#compose', function () { 31 | it('should compose functions', function () { 32 | const increment = i => i + 1, 33 | double = j => j * 2; 34 | assert.strictEqual(combinators.compose(increment, double)(2), 5); 35 | }); 36 | 37 | it('should compose multiple functions', function () { 38 | const increment = i => i + 1, 39 | double = j => j * 2, 40 | triple = i => i * 3; 41 | assert.strictEqual(combinators.compose(increment, double, triple)(1), 7); 42 | }); 43 | 44 | it('should be identity if no functions passed in', function () { 45 | assert.strictEqual(combinators.compose()(5), 5); 46 | }); 47 | }); 48 | 49 | describe('#curry', function () { 50 | it('should pass curried parameter', function () { 51 | const fn = param => param, 52 | curriedFn = combinators.curry(fn, 1); 53 | 54 | assert.strictEqual(curriedFn(), 1); 55 | }); 56 | 57 | it('should curry multiple parameters', function () { 58 | const fn = (param1, param2) => param1 + param2, 59 | curriedFn = combinators.curry(fn, 1, 2); 60 | 61 | assert.strictEqual(curriedFn(), 3); 62 | }); 63 | 64 | it('should support partial currying', function () { 65 | const fn = (param1, param2) => param1 + param2, 66 | curriedFn = combinators.curry(fn, 1); 67 | 68 | assert.strictEqual(curriedFn(2), 3); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/util/dateTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | date = require('../../src/util/date'); 5 | 6 | describe('date', function () { 7 | describe('#when', function () { 8 | it('should be today if date is the same', function () { 9 | assert.strictEqual(date.howLongAgo('2015-01-09', '2015-01-09'), 'today'); 10 | }); 11 | 12 | it('should be yesterday for 1 day ago of same month', function () { 13 | assert.strictEqual(date.howLongAgo('2015-01-09', '2015-01-10'), 'yesterday'); 14 | }); 15 | 16 | it('should be yesterday for last day of previous month if today is the first day of next month', function () { 17 | assert.strictEqual(date.howLongAgo('2015-01-31', '2015-02-01'), 'yesterday'); 18 | }); 19 | 20 | it('should be yesterday for last day of previous year if today is first day of year', function () { 21 | assert.strictEqual(date.howLongAgo('2014-12-31', '2015-01-01'), 'yesterday'); 22 | }); 23 | 24 | it('should be this week for two days ago', function () { 25 | assert.strictEqual(date.howLongAgo('2015-01-08', '2015-01-10'), 'this week'); 26 | }); 27 | 28 | it('should be this week for six days ago', function () { 29 | assert.strictEqual(date.howLongAgo('2015-01-04', '2015-01-10'), 'this week'); 30 | }); 31 | 32 | it('should be last week for seven days ago', function () { 33 | assert.strictEqual(date.howLongAgo('2015-01-03', '2015-01-10'), 'last week'); 34 | }); 35 | 36 | it('should be last week for 13 days ago', function () { 37 | assert.strictEqual(date.howLongAgo('2014-12-28', '2015-01-10'), 'last week'); 38 | }); 39 | 40 | it('should be this month for 14 days ago in same month', function () { 41 | assert.strictEqual(date.howLongAgo('2015-01-17', '2015-01-31'), 'this month'); 42 | }); 43 | 44 | it('should be this month for 30 days ago in same month', function () { 45 | // Adding time to avoid UTC conversion pushing it back a month 46 | assert.strictEqual(date.howLongAgo('2015-01-01T18:00:00.000Z', '2015-01-31'), 'this month'); 47 | }); 48 | 49 | it('should be empty fo 14 days ago last month', function () { 50 | assert.strictEqual(date.howLongAgo('2014-12-31', '2015-01-14'), ''); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/util/errorsTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | errors = require('../../src/util/errors'), 5 | inherit = require('../../src/util/inherit'); 6 | 7 | describe('errors', function () { 8 | describe('#details', function () { 9 | it('should include Error prototype properties', function () { 10 | const error = inherit.from(Error, { code: 'code' }), 11 | keys = Object.keys(errors.details(error)); 12 | 13 | assert.deepEqual(keys, ['code', 'name', 'stack']); 14 | }); 15 | 16 | it('should return own properties for non Error objects', function () { 17 | const keys = Object.keys(errors.details({ first: 1, second: 2 })); 18 | 19 | assert.deepEqual(keys, ['first', 'second']); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/util/inheritTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | inherit = require('../../src/util/inherit'); 5 | 6 | describe('inherit', function () { 7 | describe('#from', function () { 8 | it('should inherit prototype', function () { 9 | const obj = inherit.from({ prototypeKey: 'prototypeValue' }); 10 | assert.strictEqual(obj.prototypeKey, 'prototypeValue'); 11 | }); 12 | 13 | it('should have both new keys and prototype keys', function () { 14 | const obj = inherit.from({ prototypeKey: 'prototypeValue' }, { ownKey: 'ownValue' }); 15 | assert.strictEqual(obj.prototypeKey, 'prototypeValue'); 16 | assert.strictEqual(obj.ownKey, 'ownValue'); 17 | }); 18 | 19 | it('should shadow prototype with own keys', function () { 20 | const obj = inherit.from({ key: 'prototypeValue' }, { key: 'ownValue' }); 21 | assert.strictEqual(obj.key, 'ownValue'); 22 | }); 23 | 24 | it('should call new on function supers', function () { 25 | function F () { 26 | this.key = 'value'; 27 | } 28 | 29 | const obj = inherit.from(F); 30 | 31 | assert.strictEqual(obj.key, 'value'); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/util/ipTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | ip = require('../../src/util/ip'), 5 | Logger = require('../fakes/fakeLogger'); 6 | 7 | describe('ip', function () { 8 | describe('#createIPVerification', function () { 9 | let logger; 10 | 11 | beforeEach(function () { 12 | logger = Logger.create(); 13 | }); 14 | 15 | it('should accept local ips if localOnly is true', function () { 16 | const options = { 17 | localOnly: true, 18 | ipWhitelist: ['*'] 19 | }, 20 | isAllowedConnection = ip.createIPVerification(options); 21 | 22 | assert.strictEqual(true, isAllowedConnection('127.0.0.1', logger)); 23 | }); 24 | 25 | it('should not accept remote ip if localOnly is true', function () { 26 | const options = { 27 | localOnly: true, 28 | ipWhitelist: ['*'] 29 | }, 30 | isAllowedConnection = ip.createIPVerification(options); 31 | 32 | assert.strictEqual(false, isAllowedConnection('10.10.10.10', logger)); 33 | logger.warn.assertLogged('Blocking incoming connection from 10.10.10.10. Turn off --localOnly or add to --ipWhitelist to allow'); 34 | }); 35 | 36 | it('should allow any IP if localOnly is false and ipWhitelist contains *', function () { 37 | const options = { 38 | ipWhitelist: ['127.0.0.1', '*', '10.10.10.10'], 39 | localOnly: false 40 | }, 41 | isAllowedConnection = ip.createIPVerification(options); 42 | 43 | assert.strictEqual(true, isAllowedConnection('anything', logger)); 44 | }); 45 | 46 | it('should ignore a * if localOnly is true', function () { 47 | const options = { 48 | ipWhitelist: ['*'], 49 | localOnly: true 50 | }, 51 | isAllowedConnection = ip.createIPVerification(options); 52 | 53 | assert.strictEqual(false, isAllowedConnection('anything', logger)); 54 | }); 55 | 56 | it('should not block if no ip provided', function () { 57 | const options = { 58 | localOnly: true, 59 | ipWhitelist: ['*'] 60 | }, 61 | isAllowedConnection = ip.createIPVerification(options); 62 | 63 | assert.strictEqual(false, isAllowedConnection(undefined, logger)); 64 | logger.error.assertLogged('Blocking request because no IP address provided. This is likely a bug in the protocol implementation.'); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/util/scopedLoggerTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'), 4 | mock = require('../mock').mock, 5 | Logger = require('../../src/util/scopedLogger'); 6 | 7 | describe('scopedLogger', function () { 8 | describe('#create', function () { 9 | ['debug', 'info', 'warn', 'error'].forEach(level => { 10 | it('should prefix protocol name and port to all ' + level + ' calls', function () { 11 | const logger = { debug: mock(), info: mock(), warn: mock(), error: mock() }, 12 | scopedLogger = Logger.create(logger, 'prefix'); 13 | 14 | scopedLogger[level]('log %s', level); 15 | 16 | assert.ok(logger[level].wasCalledWith(`[prefix] log ${level}`), logger[level].message()); 17 | }); 18 | }); 19 | 20 | it('should allow nested scopes', function () { 21 | const logger = { debug: mock() }, 22 | scopedLogger = Logger.create(logger, 'prefix').withScope('nested'); 23 | 24 | scopedLogger.debug('log'); 25 | 26 | assert.ok(logger.debug.wasCalledWith('[prefix] nested log'), logger.debug.message()); 27 | }); 28 | 29 | it('should allow changing scope', function () { 30 | const logger = { debug: mock() }, 31 | scopedLogger = Logger.create(logger, 'original'); 32 | 33 | scopedLogger.changeScope('changed'); 34 | scopedLogger.debug('log'); 35 | 36 | assert.ok(logger.debug.wasCalledWith('[changed] log'), logger.debug.message()); 37 | }); 38 | }); 39 | }); 40 | --------------------------------------------------------------------------------