├── .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 |