├── .flake8 ├── .github ├── release-drafter.yml └── workflows │ ├── lint-docs.yml │ ├── release-drafter.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── deploy_to_pypi.sh ├── docs ├── CONTRIBUTING.md ├── api-client.md ├── changelog.md ├── examples.md ├── images │ ├── python-wiremock-horizontal.png │ ├── python-wiremock-logo.png │ ├── python-wiremock-logo.svg │ └── python-wiremock-opengraph.png ├── index.md ├── install.md ├── quickstart.md └── testcontainers.md ├── examples ├── README.md ├── intro │ ├── .flake8 │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── poetry.lock │ ├── product_mock │ │ ├── __init__.py │ │ ├── overview_service.py │ │ └── products_service.py │ ├── pyproject.toml │ └── tests │ │ ├── __init__.py │ │ ├── test_java_server.py │ │ ├── test_testcontainers.py │ │ └── utils.py └── quickstart │ ├── README.md │ └── test.py ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── requirements.pip ├── run_tests.sh ├── tests ├── conftest.py ├── test_containers.py ├── test_resources │ ├── test_base_resource.py │ ├── test_mapping │ │ ├── test_mapping_resource.py │ │ └── test_mapping_serialization.py │ ├── test_nearmisses │ │ ├── test_near_misses_resource.py │ │ └── test_near_misses_serialization.py │ ├── test_requests │ │ └── test_requests_resource.py │ ├── test_scenarios │ │ ├── test_scenario_resource.py │ │ └── test_scenario_serialization.py │ └── test_settings │ │ ├── test_settings_resource.py │ │ └── test_settings_serialization.py ├── test_responses │ └── test_requests │ │ └── test_requests_serialization.py ├── test_server │ └── test_server.py └── utils.py ├── tox.ini └── wiremock ├── VERSION ├── __init__.py ├── _compat.py ├── base ├── __init__.py ├── base_entity.py └── base_resource.py ├── client.py ├── constants.py ├── exceptions ├── __init__.py ├── api_exception.py ├── api_unavailable_exception.py ├── client_exception.py ├── forbidden_exception.py ├── invalid_input_exception.py ├── not_found_exception.py ├── requires_login_exception.py ├── server_exception.py ├── timeout_exception.py └── unexpected_response_exception.py ├── resources ├── __init__.py ├── mappings │ ├── __init__.py │ ├── models.py │ └── resource.py ├── near_misses │ ├── __init__.py │ ├── models.py │ └── resource.py ├── requests │ ├── __init__.py │ ├── models.py │ └── resource.py ├── scenarios │ ├── __init__.py │ └── resource.py └── settings │ ├── __init__.py │ ├── models.py │ └── resource.py ├── server ├── __init__.py ├── exceptions.py ├── server.py └── wiremock-standalone-2.35.1.jar └── testing ├── __init__.py └── testcontainer.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E226,E302,E41 3 | max-line-length = 88 4 | max-complexity = 10 5 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: $NEXT_MINOR_VERSION 3 | tag-template: $NEXT_MINOR_VERSION 4 | 5 | # Emoji reference: https://gitmoji.carloscuesta.me/ 6 | categories: 7 | - title: 💥 Breaking changes 8 | labels: 9 | - breaking 10 | - title: 🚀 New features and improvements 11 | labels: 12 | - enhancement 13 | - title: 🐛 Bug fixes 14 | labels: 15 | - bug 16 | - title: 📝 Documentation updates 17 | labels: 18 | - documentation 19 | - title: 🌐 Localization and translation 20 | labels: 21 | - localization 22 | - title: 🌐 Community-related changes 23 | labels: 24 | - community 25 | - title: 👻 Maintenance 26 | labels: 27 | - chore 28 | - maintenance 29 | - title: 🚦 Tests 30 | labels: 31 | - test 32 | - title: ✍ Other changes 33 | # Default label used by Dependabot 34 | - title: 📦 Dependency updates 35 | labels: 36 | - dependencies 37 | collapse-after: 15 38 | exclude-labels: 39 | - skip-changelog 40 | - invalid 41 | 42 | template: | 43 | 44 | $CHANGES 45 | 46 | autolabeler: 47 | - label: "documentation" 48 | files: 49 | - "*.md" 50 | branch: 51 | - '/docs{0,1}\/.+/' 52 | - label: "bug" 53 | branch: 54 | - '/fix\/.+/' 55 | title: 56 | - "/fix/i" 57 | - label: "enhancement" 58 | branch: 59 | - '/feature\/.+/' 60 | -------------------------------------------------------------------------------- /.github/workflows/lint-docs.yml: -------------------------------------------------------------------------------- 1 | name: Verify documentation 2 | on: 3 | push: 4 | branches: ["main", "master"] 5 | pull_request: 6 | branches: ["main", "master"] 7 | 8 | jobs: 9 | verify-docs: 10 | name: Check Documentation 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.10" 19 | - name: Install and configure Poetry 20 | uses: snok/install-poetry@v1 21 | with: 22 | version: 1.5.1 23 | - name: Install dependencies 24 | run: | 25 | poetry env use '3.10' 26 | poetry install --extras=testing 27 | - name: Build docs with MkDocs 28 | run: | 29 | make docs 30 | - name: Check Markdown links 31 | uses: gaurav-nelson/github-action-markdown-link-check@v1 32 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | - name: Install Poetry 18 | run: curl -sSL https://install.python-poetry.org | python 19 | - name: Get version from pyproject.toml 20 | run: echo "VERSION=$(poetry version -s)" >> $GITHUB_ENV 21 | - uses: release-drafter/release-drafter@v5 22 | with: 23 | name: ${{ env.VERSION }} 24 | tag: ${{ env.VERSION }} 25 | version: ${{ env.VERSION }} 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-publish: 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: pypi 12 | url: https://pypi.org/p/wiremock 13 | permissions: 14 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.10" 21 | - name: Install and configure Poetry 22 | uses: snok/install-poetry@v1 23 | with: 24 | version: 1.5.1 25 | - name: Install dependencies 26 | run: poetry install 27 | - name: Build distributions 28 | run: poetry build 29 | - name: Publish to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | unit-tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install and configure Poetry 25 | uses: snok/install-poetry@v1 26 | with: 27 | version: 1.5.1 28 | 29 | - name: Install dependencies 30 | run: | 31 | poetry env use ${{matrix.python-version}} 32 | poetry install --extras=testing 33 | 34 | - name: Test with pytest 35 | run: | 36 | poetry run pytest tests/ 37 | 38 | integration-tests: 39 | runs-on: ubuntu-latest 40 | needs: [unit-tests] 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | 45 | - name: Integration Tests 46 | run: | 47 | cd examples/intro/ 48 | docker compose build overview_srv 49 | docker compose run overview_srv pytest --tb=short 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | coverage/ 45 | wellaware/tests/coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | ### JetBrains template 100 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 101 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 102 | 103 | # User-specific stuff: 104 | .idea/ 105 | 106 | *.iml 107 | 108 | ## File-based project format: 109 | *.iws 110 | 111 | ## Plugin-specific files: 112 | 113 | # IntelliJ 114 | /out/ 115 | 116 | # mpeltonen/sbt-idea plugin 117 | .idea_modules/ 118 | 119 | # JIRA plugin 120 | atlassian-ide-plugin.xml 121 | 122 | # Crashlytics plugin (for Android Studio and IntelliJ) 123 | com_crashlytics_export_strings.xml 124 | crashlytics.properties 125 | crashlytics-build.properties 126 | fabric.properties 127 | ### VirtualEnv template 128 | # Virtualenv 129 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 130 | .Python 131 | [Bb]in 132 | [Ii]nclude 133 | [Ll]ib 134 | [Ll]ib64 135 | [Ll]ocal 136 | [Ss]cripts 137 | pyvenv.cfg 138 | .venv 139 | pip-selfcheck.json 140 | ### macOS template 141 | *.DS_Store 142 | .AppleDouble 143 | .LSOverride 144 | 145 | # Icon must end with two \r 146 | Icon 147 | 148 | 149 | # Thumbnails 150 | ._* 151 | 152 | # Files that might appear in the root of a volume 153 | .DocumentRevisions-V100 154 | .fseventsd 155 | .Spotlight-V100 156 | .TemporaryItems 157 | .Trashes 158 | .VolumeIcon.icns 159 | .com.apple.timemachine.donotpresent 160 | 161 | # Directories potentially created on remote AFP share 162 | .AppleDB 163 | .AppleDesktop 164 | Network Trash Folder 165 | Temporary Items 166 | .apdisk 167 | 168 | # Static docs 169 | html/ 170 | site/ 171 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | formats: 9 | - pdf 10 | - epub 11 | 12 | # Build documentation with MkDocs 13 | mkdocs: 14 | configuration: mkdocs.yml 15 | 16 | # Set the version of Python and other tools you might need 17 | build: 18 | os: ubuntu-22.04 19 | tools: 20 | python: "3.11" 21 | jobs: 22 | post_create_environment: 23 | # Install poetry 24 | # https://python-poetry.org/docs/#installing-manually 25 | - pip install poetry 26 | # Tell poetry to not use a virtual environment 27 | - poetry config virtualenvs.create false 28 | post_install: 29 | # Install dependencies with 'docs' dependency group 30 | # https://python-poetry.org/docs/managing-dependencies/#dependency-groups 31 | - poetry install --with docs 32 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Cody Lee 2 | Mikey Waites 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Cody Lee. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include wiremock *.py VERSION wiremock-standalone-*.jar 5 | recursive-exclude wiremock/tests 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc ext-test test upload-docs docs coverage 2 | 3 | all: clean black test coverage 4 | 5 | black: 6 | black -v -l 150 --include wiremock/*.py 7 | black -v -l 150 --include wiremock/base/*.py 8 | black -v -l 150 --include wiremock/exceptions/*.py 9 | black -v -l 150 --include wiremock/resources/*.py 10 | black -v -l 150 --include wiremock/resources/mappings/*.py 11 | black -v -l 150 --include wiremock/resources/near_misses/*.py 12 | black -v -l 150 --include wiremock/resources/requests/*.py 13 | black -v -l 150 --include wiremock/resources/scenarios/*.py 14 | black -v -l 150 --include wiremock/resources/settings/*.py 15 | black -v -l 150 --include wiremock/server/*.py 16 | black -v -l 150 --include wiremock/tests/*.py 17 | black -v -l 150 --include wiremock/tests/base_tests/*.py 18 | black -v -l 150 --include wiremock/tests/resource_tests/*.py 19 | black -v -l 150 --include wiremock/tests/resource_tests/mappings_tests/*.py 20 | black -v -l 150 --include wiremock/tests/resource_tests/near_misses_tests/*.py 21 | black -v -l 150 --include wiremock/tests/resource_tests/requests_tests/*.py 22 | black -v -l 150 --include wiremock/tests/resource_tests/scenarios_tests/*.py 23 | black -v -l 150 --include wiremock/tests/resource_tests/settings_tests/*.py 24 | black -v -l 150 --include wiremock/tests/server_tests/*.py 25 | 26 | test: 27 | bash run_tests.sh 28 | 29 | coverage: 30 | bash run_coverage.sh 31 | 32 | release: 33 | bash deploy_to_pypi.sh 34 | 35 | tox-test: 36 | PYTHONDONTWRITEBYTECODE= tox 37 | 38 | clean: 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '#*' -exec rm -f {} + 43 | find . -name '.#*' -exec rm -f {} + 44 | find . -name '.bak' -exec rm -f {} + 45 | 46 | docs: 47 | poetry run mkdocs build --site-dir=html 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python WireMock 2 | 3 |

4 | 5 | WireMock Logo 6 | 7 |

8 | 9 | Python WireMock is a library that allows users to interact with a WireMock instance from within a Python project. 10 | Full documentation can be found at [wiremock.readthedocs.org](http://wiremock.readthedocs.org/). 11 | 12 | [![a](https://img.shields.io/badge/slack-%23wiremock%2Fpython-brightgreen?style=flat&logo=slack)](https://slack.wiremock.org/) 13 | [![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg)](http://wiremock.readthedocs.org/) 14 | 15 | 19 | 20 | ## Key Features 21 | 22 | WireMock can run in unit tests, as a standalone process or a container. Key features include: 23 | 24 | - [Testcontainers Python](https://github.com/testcontainers/testcontainers-python) module to easily start WireMock server for your tests 25 | - REST API Client for a standalone WireMock Java server 26 | - Support for most of major [WireMock features ](https://wiremock.org/docs) (more on their way soon) 27 | 28 | ## References 29 | 30 | - [Quickstart Guide](./docs/quickstart.md) 31 | - [Installation](./docs/install.md) 32 | - [Full documentation](http://wiremock.readthedocs.org/) 33 | 34 | ## Examples 35 | 36 | There are several [example projects](./examples/) included to demonstrate the different ways that WireMock can be used to mock 37 | services in your tests and systems. The example test modules demonstrate different strategies for testing against 38 | the same "product service" and act as a good demonstration of real world applications to help you get started. 39 | 40 | - [Testcontainers Python](examples/intro/tests/test_testcontainers.py) 41 | - [Standalone Java Server Version](examples/intro/tests/test_java_server.py) 42 | 43 | ## Contributing 44 | 45 | See the [Contributor Guide](./docs/CONTRIBUTING.md) 46 | -------------------------------------------------------------------------------- /deploy_to_pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Deploy to PyPI for both source and wheel 4 | # 5 | rm -Rf build/ dist/ wiremock.egg-info/ || true 6 | python3 -m build --sdist --wheel 7 | python3 -m twine upload dist/* 8 | rm -Rf build/ dist/ wiremock.egg-info/ || true 9 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Python WireMock 2 | 3 | [![a](https://img.shields.io/badge/slack-%23wiremock%2Fpython-brightgreen?style=flat&logo=slack)](https://slack.wiremock.org/) 4 | 5 | WireMock exists and continues to thrive due to the efforts of over 150 contributors, and we continue to welcome contributions to its evolution. Regardless of your expertise and time you could dedicate, there’re opportunities to participate and help the project! 6 | 7 | This page covers contributing to _Python WireMock_. 8 | For generic guidelines and links to other technology stacks, 9 | see [this page](https://wiremock.org/docs/participate/). 10 | 11 | ## Get Started 12 | 13 | 1. Join us ion the `#wiremock-python` channel on the [WireMock Slack](https://slack.wiremock.org/) 14 | 2. Check out the GitHub issues! 15 | 16 | ## Pull Requests 17 | 18 | All patches to the repository are done via pull requests. 19 | No special prerequisites exist, you can just submit the patches! 20 | General expectations: 21 | 22 | - All Tests and static checkers must pass 23 | - Code coverage shouldn't decrease 24 | - All Pull Requests should be rebased against master **before** submitting the PR. 25 | - The Pull Request titles represent the change well for users or developers 26 | 27 | ## Development 28 | 29 | We use [Poetry](https://python-poetry.org/) for packaging and dependency management. 30 | 31 | After forking and cloning the repository, 32 | run the following command to setup the project: 33 | 34 | `poetry install` 35 | 36 | Then use your favorite IDE for development and testing. 37 | 38 | ## Contributing examples 39 | 40 | Please submit new examples as a pull requests to the 41 | [examples directory](https://github.com/wiremock/python-wiremock/tree/master/examples). 42 | You can also also add links to external examples and tutorials to the `README.md` 43 | file in the directory. 44 | 45 | When adding new examples, 46 | make sure to update the [documentation site page](./examples.md) too. 47 | 48 | ## Working on Documentation 49 | 50 | The documentation is powered by [MkDocs](https://www.mkdocs.org/) and [ReadTheDocs](https://readthedocs.org/). 51 | All the necessary dependencies are included into the Poetry definition. 52 | To build the docs locally: 53 | 54 | ```bash 55 | poetry run mkdocs build --site-dir=html 56 | ``` 57 | 58 | MkDocs also comes with a built-in dev-server that lets you preview your documentation as you work on it by running the `mkdocs serve` command. 59 | By default, it will deploy the live documentation site to `http://localhost:8000`. 60 | 61 | ## See also 62 | 63 | - [Contributing to WireMock](https://wiremock.org/docs/participate/) 64 | -------------------------------------------------------------------------------- /docs/api-client.md: -------------------------------------------------------------------------------- 1 | Using with a Standalone WireMock 2 | =========== 3 | 4 | An example app: 5 | 6 | ```python 7 | from wiremock.constants import Config 8 | from wiremock.client import * 9 | 10 | Config.base_url = 'https://mockserver.example.com/__admin/' 11 | # Optionally set a custom cert path: 12 | # Config.requests_cert = ... (See requests documentation) 13 | # Optionally disable cert verification 14 | # Config.requests_verify = False 15 | 16 | mapping = Mapping( 17 | priority=100, 18 | request=MappingRequest( 19 | method=HttpMethods.GET, 20 | url='/hello' 21 | ), 22 | response=MappingResponse( 23 | status=200, 24 | body='hi' 25 | ), 26 | persistent=False, 27 | ) 28 | 29 | mapping = Mappings.create_mapping(mapping=mapping) 30 | 31 | all_mappings = Mappings.retrieve_all_mappings() 32 | ``` 33 | 34 | ### Starting WireMock server with a context manager 35 | 36 | ```python 37 | from wiremock.constants import Config 38 | from wiremock.client import * 39 | from wiremock.server.server import WireMockServer 40 | 41 | with WireMockServer() as wm: 42 | Config.base_url = 'http://localhost:{}/__admin'.format(wm.port) 43 | Mappings.create_mapping(...) # Set up stubs 44 | requests.get(...) # Make API calls 45 | ``` 46 | 47 | ### Starting WireMock server in a unittest.TestCase 48 | 49 | ```python 50 | 51 | class MyTestClassBase(TestCase): 52 | @classmethod 53 | def setUpClass(cls): 54 | wm = self.wiremock_server = WireMockServer() 55 | wm.start() 56 | Config.base_url = 'http://localhost:{}/__admin'.format(wm.port) 57 | 58 | @classmethod 59 | def tearDownClass(cls): 60 | self.wiremock_server.stop() 61 | ``` 62 | 63 | ### Customizing the path to java 64 | 65 | ```python 66 | WireMockServer(java_path='/path/to/my/java') 67 | ``` 68 | 69 | ### Customizing the WireMock server JAR file: 70 | 71 | ```python 72 | WireMockServer(jar_path='/my/secret/location/wiremock-standalone-2.35.1.jar') 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ========= 3 | 4 | Changes to the library are recorded here. 5 | 6 | Newer version 7 | ------ 8 | 9 | See [GitHub Releases](https://github.com/wiremock/python-wiremock/releases) 10 | 11 | v2.4.0 12 | ------ 13 | 14 | Enhancements: 15 | 16 | - Adds --root\_dir option (\#40) @dtwwolinski 17 | 18 | Maintenance: 19 | 20 | - Automates release documentation and pypi distribution (\#63) 21 | @mikeywaites 22 | - Removes setuptools dependency on build (\#62) @jmdacruz 23 | - Uses assertEqual instead of assertEquals for Python 3.11 24 | compatibility. (\#45) @tirkarthi 25 | - Updates to modern Python tooling (\#57) @mikeywaites 26 | - Sets up working example of python-wiremock in the repo (\#59) 27 | @mikeywaites 28 | - Removes travis config file (\#61) @mikeywaites 29 | - Adds imports to quickstart example (\#42) @jmendesky 30 | 31 | v2.3.1 32 | ------ 33 | 34 | > - Adds support for --root\_dir option on startup thanks to 35 | > @dtwwolinski 36 | 37 | v2.3.0 38 | ------ 39 | 40 | > - Adds missing Metadata delete calls thanks to @andreroggeri 41 | 42 | v2.2.0 43 | ------ 44 | 45 | > - Adds missing Metadata serde options thanks to @andreroggeri 46 | 47 | v2.1.3 48 | ------ 49 | 50 | > - Fix on startup thanks to @Enforcer 51 | 52 | v2.1.2 53 | ------ 54 | 55 | > - Python3.8 lint fixes thanks to @tirkarthi 56 | 57 | v2.1.1 58 | ------ 59 | 60 | > - Fixes startup error on connection error thanks to @vasuarez 61 | 62 | v2.1.0 63 | ------ 64 | 65 | > - Enables Templating thanks to @mauricioalarcon 66 | 67 | v2.0.0 68 | ------ 69 | 70 | > - Fixes issue \#14 71 | > - Drops support for Python 2.x as this is EOL. 72 | 73 | v1.2.0 74 | ------ 75 | 76 | > - Add custom cert/verification options to be passed normally through 77 | > the singleton config 78 | > - Upgrades minimum requests version to 2.20.0 for known 79 | > CVE-2018-18074 80 | 81 | v1.1.5 82 | ------ 83 | 84 | > - Looser requirements everywhere, run free! 85 | 86 | v1.1.4 87 | ------ 88 | 89 | > - Update links in setup.py and docs 90 | 91 | v1.1.3 92 | ------ 93 | 94 | > - Looser dependency constraint in setup.py 95 | 96 | v1.1.2 97 | ------ 98 | 99 | > - Allow looser dependency constraint for requests 100 | 101 | v1.1.1 102 | ------ 103 | 104 | > - Fixed bug when wiremock jar not found. 105 | 106 | v1.1.0 107 | ------ 108 | 109 | > - Added Ability to stand up wiremock server (requires standalone jar 110 | > being available). 111 | 112 | v1.0.3 113 | ------ 114 | 115 | > - Fix support for Python 3.4. 116 | 117 | v1.0.2 118 | ------ 119 | 120 | > - Wiremock 2.6.x uses 201 response code instead. 121 | 122 | v1.0.1 123 | ------ 124 | 125 | > - Bug Fix on dictionary klass deserialization fix. 126 | 127 | v1.0.0 128 | ------ 129 | 130 | > - First Release - included admin feature set for wiremock 2.5.1 131 | > standalone 132 | 133 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Python WireMock examples 2 | 3 | - [quickstart](https://github.com/wiremock/python-wiremock/tree/master/examples/quickstart) - 4 | example for the[Quick Start Guide](./quickstart.md) 5 | - [intro](https://github.com/wiremock/python-wiremock/tree/master/examples/intro) - 6 | End-to-End example for both Testcontainers module 7 | and native `pytest` integration. 8 | 9 | ## External examples 10 | 11 | No external examples are referenced at the moment. 12 | Please be welcome to [contribute](./CONTRIBUTING.md)! 13 | 14 | ## Contributing examples 15 | 16 | More examples are always welcome! 17 | See the [Contributor Guide](./CONTRIBUTING.md). 18 | -------------------------------------------------------------------------------- /docs/images/python-wiremock-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/docs/images/python-wiremock-horizontal.png -------------------------------------------------------------------------------- /docs/images/python-wiremock-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/docs/images/python-wiremock-logo.png -------------------------------------------------------------------------------- /docs/images/python-wiremock-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 58 | 60 | 62 | 66 | 70 | 71 | 73 | 77 | 81 | 82 | 84 | 88 | 92 | 93 | 95 | 99 | 103 | 104 | 106 | 110 | 114 | 115 | 117 | 121 | 125 | 126 | 135 | 144 | 154 | 164 | 174 | 184 | 194 | 204 | 215 | 226 | 227 | 231 | 235 | 242 | 243 | -------------------------------------------------------------------------------- /docs/images/python-wiremock-opengraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/docs/images/python-wiremock-opengraph.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Python WireMock 2 | 3 | Python WireMock is an HTTP client that allows users to interact with a WireMock instance from within a Python project. 4 | 5 | [![a](https://img.shields.io/badge/slack-%23wiremock%2Fpython-brightgreen?style=flat&logo=slack)](https://slack.wiremock.org/) 6 | 7 | ## Key Features 8 | 9 | WireMock can run in unit tests, as a standalone process or a container, or in the cloud. 10 | Python WireMock enables all these usage modes. 11 | Key features include: 12 | 13 | - [Testcontainers Python](https://github.com/testcontainers/testcontainers-python) module to easily start WireMock server for your tests 14 | - REST API Client for a standalone WireMock Java server 15 | - Supports most of the major [Wiremock](https://wiremock.org/docs) features (more on their way soon) 16 | 17 | ## Documentation 18 | 19 | - [Quickstart](./quickstart.md) 20 | - [Installation](./install.md) 21 | - [Testcontainers module](./testcontainers.md) 22 | - [Using with standalone WireMock](./api-client.md) 23 | 24 | ## Links 25 | 26 | - WireMock Standalone: 27 | - Documentation: 28 | - Official Repository: 29 | - Package: 30 | 31 | 44 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | Python WireMock is known to support Python 3.7 or above. 6 | 7 | ## Pip 8 | 9 | To install: 10 | 11 | `pip install wiremock` 12 | 13 | To install with testing dependencies: 14 | 15 | `pip install wiremock[testing]` 16 | 17 | ## Poetry 18 | 19 | To install via Poetry: 20 | 21 | `poetry add wiremock` 22 | 23 | Or: 24 | 25 | ```bash 26 | git clone [TODO](https://github.com/wiremock/python-wiremock.git) 27 | poetry install 28 | ``` 29 | 30 | To install with testing dependencies: 31 | 32 | `poetry add --extras=testing wiremock` 33 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | The preferred way of using WireMock to mock your services is by using the provided `WireMockContainer` 4 | that uses [testcontainers-python](https://github.com/testcontainers/testcontainers-python) 5 | and provisions WireMock as a test container on-demand. 6 | 7 | In this example we will use the [pytest](https://docs.pytest.org/) framework. 8 | 9 | ## Prerequisites 10 | 11 | - Python 3.7 or above 12 | - Pip 20.0.0 or above (use `apt install python3-pip`, for example) 13 | - Pytest 7.3.0 or above (use `pip install pytest`) 14 | - Testcontainers 3.5.0 or above (use `pip install testcontainers`) 15 | 16 | ## Install Python WireMock 17 | 18 | To install the most recent version of the Python WireMock library, 19 | use the following command: 20 | 21 | ```bash 22 | pip install wiremock 23 | ``` 24 | 25 | ## Create the Test Fixture 26 | 27 | As a first step, we will need to provision a test WireMock server to be used in tests: 28 | 29 | 1. Create an empty `test.py` file 30 | 2. In this file, create a pytest fixture to manage the container life-cycle. 31 | Use fixture `scope` to control how often the container is created 32 | 3. Set the WireMock SDK config URL to the URL exposed by the container. 33 | It will route all Admin API requests to 34 | the mock server. 35 | 4. Create REST API stub mapping for the `/hello` endpoint using the Admin SDK. 36 | 37 | ```python 38 | import pytest 39 | import requests 40 | 41 | from wiremock.testing.testcontainer import wiremock_container 42 | from wiremock.constants import Config 43 | from wiremock.client import * 44 | 45 | @pytest.fixture # (1) 46 | def wiremock_server(): 47 | with wiremock_container(secure=False) as wm: 48 | Config.base_url = wm.get_url("__admin") # (2) 49 | Mappings.create_mapping( 50 | Mapping( 51 | request=MappingRequest(method=HttpMethods.GET, url="/hello"), 52 | response=MappingResponse(status=200, body="hello"), 53 | persistent=False, 54 | ) 55 | ) # (3) 56 | yield wm 57 | ``` 58 | 59 | ## Write your first test with WireMock 60 | 61 | Use the `wiremock_server` fixture in your tests and make requests against the mock server. 62 | Add the following test to the `test.py` file: 63 | 64 | ```python 65 | def test_get_hello_world(wiremock_server): # (4) 66 | response = requests.get(wiremock_server.get_url("/hello")) 67 | 68 | assert response.status_code == 200 69 | assert response.content == b"hello" 70 | ``` 71 | 72 | ## Run the test! 73 | 74 | Run the following command: 75 | 76 | ```bash 77 | pytest test.py -v 78 | ``` 79 | 80 | Sample output: 81 | 82 | ``` 83 | $ pytest test.py -v 84 | ================ test session starts ================ 85 | platform linux -- Python 3.8.10, pytest-7.4.0, pluggy-1.2.0 -- /usr/bin/python3 86 | cachedir: .pytest_cache 87 | rootdir: /c/Users/Oleg/Documents/opensource/wiremock/python-wiremock 88 | configfile: pyproject.toml 89 | plugins: anyio-3.7.1 90 | collected 1 item 91 | 92 | test.py::test_get_hello_world PASSED [100%] 93 | ``` 94 | 95 | ## Read More 96 | 97 | You can read more about Testcontainers support in Python WireMock [here](./testcontainers.md). 98 | 99 | ## More examples 100 | 101 | See [this page](./examples.md) for more example references 102 | -------------------------------------------------------------------------------- /docs/testcontainers.md: -------------------------------------------------------------------------------- 1 | # Testcontainers module 2 | 3 | Python WireMock ships with support for [testcontainers-wiremock](https://github.com/testcontainers/testcontainers-python) to easily start WireMock server from your test suite using Python. 4 | 5 | ## Using the context manager 6 | 7 | The simplest way to integrate the WireMock container into your test suite is to use the `wiremock_container` context manager. For pytest users this can be 8 | used in conjuction with a pytest fixture to easily manage the life-cycle of the container. 9 | 10 | ```python 11 | import pytest 12 | 13 | from wiremock.testing.testcontainer import wiremock_container 14 | 15 | @pytest.fixture(scope="session") # (1) 16 | def wm_server(): 17 | with wiremock_container(secure=False) as wm: 18 | 19 | Config.base_url = wm.get_url("__admin") # (2) 20 | 21 | Mappings.create_mapping( 22 | Mapping( 23 | request=MappingRequest(method=HttpMethods.GET, url="/hello"), 24 | response=MappingResponse(status=200, body="hello"), 25 | persistent=False, 26 | ) 27 | ) # (3) 28 | yield wm 29 | 30 | 31 | def test_get_hello_world(wm_server): # (4) 32 | 33 | resp1 = requests.get(wm_server.get_url("/hello"), verify=False) 34 | 35 | assert resp1.status_code == 200 36 | assert resp1.content == b"hello" 37 | ``` 38 | 39 | 1. Create a pytest fixture to manage the container life-cycle. use fixture `scope` to control how often the container is created 40 | 41 | 2. Set the wiremock sdk config url to the url exposed by the container 42 | 43 | 3. Create response and request mappings using the Admin SDK. 44 | 45 | 4. Use the `wm_server` fixture in your tests and make requests against the mock server. 46 | 47 | The context manager will automatically start the container. This is typically what you want as any attempts to generate urls to the contianer when it's not running will result in errors. 48 | 49 | If you do need to start the container manually yourself, you can pass `start=False` to the context manager and the context manager will simply yield the instance of the container without starting it. 50 | 51 | The `wiremock_container` also supports generating mapping request and response files for you via the mappings kwarg. 52 | 53 | ```python 54 | 55 | @pytest.mark.container_test 56 | def test_configure_via_wiremock_container_context_manager(): 57 | 58 | mappings = [ 59 | ( 60 | "hello-world.json", 61 | { 62 | "request": {"method": "GET", "url": "/hello"}, 63 | "response": {"status": 200, "body": "hello"}, 64 | }, 65 | ) 66 | ] 67 | 68 | with wiremock_container(mappings=mappings, verify_ssl_certs=False) as wm: 69 | 70 | resp1 = requests.get(wm.get_url("/hello"), verify=False) 71 | assert resp1.status_code == 200 72 | assert resp1.content == b"hello" 73 | ``` 74 | 75 | The `wiremock_container` context manager offers a number of other useful options to help to configure the container. See the `wirewmock.testing.testcontainer.wiremock_container` method for the full description 76 | of options. 77 | 78 | ## Using the WireMockContainer directly 79 | 80 | You can also instantiate the container instance yourself using `WireMockContainer`. The container itself provides methods for creating mapping files and stubs on the container instance which can be used as an alternative 81 | if you maintain your request and response stubs as files. 82 | 83 | ```python 84 | WireMockContainer(verify_ssl_certs=False) 85 | .with_mapping( 86 | "hello-world.json", 87 | { 88 | "request": {"method": "GET", "url": "/hello"}, 89 | "response": {"status": 200, "body": "hello"}, 90 | }, 91 | ) 92 | .with_mapping( 93 | "hello-world-file.json", 94 | { 95 | "request": {"method": "GET", "url": "/hello2"}, 96 | "response": {"status": 200, "bodyFileName": "hello.json"}, 97 | }, 98 | ) 99 | .with_file("hello.json", {"message": "Hello World !"}) 100 | .with_cli_arg("--verbose", "") 101 | .with_cli_arg("--root-dir", "/home/wiremock") 102 | .with_env("JAVA_OPTS", "-Djava.net.preferIPv4Stack=true") 103 | ) 104 | ``` 105 | 106 | ## Using WireMockContainer inside Docker (dind) 107 | 108 | It's common that you might need to start Testcontainers from inside of another container. The example project in `example/docker-compose.yml` actually does this. 109 | 110 | When running spawning testcontainer inside of another container you will need to set the `WIREMOCK_DIND` config variable to true. When this env var is set the host of the wiremock container 111 | will explicitly be set to `host.docker.internal`. 112 | 113 | Let's take a look at the example docker-compose.yaml the example products service uses. 114 | 115 | ```yaml 116 | version: "3" 117 | 118 | services: 119 | overview_srv: 120 | build: 121 | context: ../ 122 | dockerfile: examples/intro/Dockerfile 123 | ports: 124 | - "5001:5001" 125 | environment: 126 | - WIREMOCK_DIND=true # (1) Set the env var 127 | extra_hosts: 128 | - "host.docker.internal:host-gateway" # (2) 129 | volumes: 130 | - /var/run/docker.sock:/var/run/docker.sock # (3) 131 | - ..:/app/ 132 | - .:/app/example/ 133 | command: uvicorn product_mock.overview_service:app --host=0.0.0.0 --port=5001 134 | ``` 135 | 136 | 1. Set the environment variable to instruct WireMockContainer that we're running in `DIND` mode. 137 | 138 | 2. Map the host.docker.internal to host-gateway. Docker will magically replace the host-gateway value with the ip of the container. 139 | This mapping is required when using dind on certain CI system like github actions. 140 | 141 | 3. Mount the docker binary into the container 142 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Python WireMock examples 2 | 3 | - [Quick Start Guide](./quickstart/) 4 | - [End-to-End Example](./intro/) for 5 | both Testcontainers module and native 6 | pytest integration. 7 | 8 | ## External examples 9 | 10 | No external examples are referenced at the moment. 11 | Please be welcome to [contribute](../docs/CONTRIBUTING.md)! 12 | 13 | ## Contributing 14 | 15 | Feel free to contribute the examples! 16 | -------------------------------------------------------------------------------- /examples/intro/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, F403 3 | max-line-length = 88 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /examples/intro/Dockerfile: -------------------------------------------------------------------------------- 1 | # vim: set filetype=Dockerfile: 2 | FROM python:3.11-slim-buster 3 | 4 | ENV POETRY_HOME="/opt/poetry" \ 5 | POETRY_VIRTUALENVS_CREATE=false \ 6 | POETRY_NO_INTERACTION=1 7 | 8 | # Prepend poetry and venv to path 9 | ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" 10 | 11 | RUN apt-get update && apt-get install --no-install-recommends -y curl default-jre-headless \ 12 | && curl -sSL https://install.python-poetry.org | python 13 | 14 | # Update poetry to latest version 15 | RUN poetry self update 16 | 17 | WORKDIR /app/example 18 | 19 | # Install dependencies 20 | COPY . /app 21 | COPY ./examples/intro/pyproject.toml ./examples/intro/poetry.lock /app/example/ 22 | RUN poetry config virtualenvs.create false \ 23 | && poetry install --no-root --no-interaction --no-ansi 24 | 25 | 26 | # Copy the rest of the application code 27 | COPY ./examples/intro /app/example 28 | 29 | # Expose the port that the FastAPI app will listen on 30 | EXPOSE 5001 31 | EXPOSE 5002 32 | 33 | CMD ["python"] 34 | -------------------------------------------------------------------------------- /examples/intro/README.md: -------------------------------------------------------------------------------- 1 | # Python Wiremock End-to-End Example 2 | 3 | ## Introduction 4 | 5 | This example code demonstrates usage of python wiremock. The example code demonstrates a fictional set of microservices 6 | where the `overview_service` depends on the display of products returned from the `products_service`. 7 | 8 | When the services are run normally the Overview service makes a request to the Products services and the list of 9 | products returned by the Product Service is displayed to the user. 10 | 11 | ``` 12 | +-----------------+ +-----------------+ 13 | | Overview Service| | Products Service| 14 | +-----------------+ +-----------------+ 15 | | | 16 | | GET /products | 17 | |--------------------------------->| 18 | | | 19 | | List of products | 20 | |<---------------------------------| 21 | | | 22 | | Display products to user | 23 | |--------------------------------->| 24 | | | 25 | ``` 26 | 27 | When we are writing tests to ensure that the Overview Service works correctly, we do not want to have the overview service 28 | have to depend on real requests being made to the products services. This normally leads to the code being mocked, and other approaches that 29 | are hard to maintain and dont allow us to test the code closer to real world conditions. 30 | 31 | Ultimately, we want a real http request to happen under test conditions so that the tests and the code operate as if the products 32 | services is running and returning actual http responses. In tests we also need to be able to control what this system returns 33 | so that we can assert the code acts in certain ways when it gets certain responses from the server. 34 | 35 | This is what python-wiremock helps to solve. 36 | 37 | The example code demonstrates how to use python-wiremock to run a test server directly from our tests that give us 38 | full control over how the mock service should handle our requests. We can generate respones directly from the tests 39 | to allow us to write solid integration tests that dont involved mockig the code we're trying to test. 40 | 41 | ## Running the tests 42 | 43 | To run the tests use docker-compose to create the necessary containers. 44 | 45 | `docker compose run overview_srv pytest --tb=short` 46 | 47 | ## How we use this example code base 48 | 49 | As well as serving as a working example of how to work with python wiremock, this example code base is also used as a "e2e" test suite of sorts. 50 | The docker-compose configuration bundles the python-wiremock code directly into the container so that we can actually iterate on changes against a 51 | real world example. 52 | -------------------------------------------------------------------------------- /examples/intro/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | overview_srv: 5 | build: 6 | context: ../../ 7 | dockerfile: examples/intro/Dockerfile 8 | ports: 9 | - "5001:5001" 10 | environment: 11 | - WIREMOCK_DIND=true 12 | extra_hosts: 13 | - "host.docker.internal:host-gateway" 14 | volumes: 15 | - /var/run/docker.sock:/var/run/docker.sock 16 | - ../../:/app/ 17 | - .:/app/example/ 18 | command: uvicorn product_mock.overview_service:app --host=0.0.0.0 --port=5001 19 | 20 | products_srv: 21 | build: 22 | context: ../../ 23 | dockerfile: examples/intro/Dockerfile 24 | ports: 25 | - "5002:5002" 26 | volumes: 27 | - ../../:/app/ 28 | - .:/app/example/ 29 | command: uvicorn product_mock.products_service:app --host=0.0.0.0 --port=5002 30 | -------------------------------------------------------------------------------- /examples/intro/product_mock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/examples/intro/product_mock/__init__.py -------------------------------------------------------------------------------- /examples/intro/product_mock/overview_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | from fastapi import FastAPI 5 | 6 | app = FastAPI() 7 | 8 | 9 | @app.get("/") 10 | async def read_root(): 11 | return {"Hello": "World"} 12 | 13 | 14 | @app.get("/overview") 15 | async def read_products(category: str = "", sort_by: str = ""): 16 | PRODUCTS_SERVICE_HOST = os.environ.get( 17 | "PRODUCTS_SERVICE_HOST", "http://products_srv:5002" 18 | ) 19 | 20 | PRODUCTS_URL = f"{PRODUCTS_SERVICE_HOST}/products" 21 | 22 | params = {} 23 | if category != "": 24 | params["category"] = category 25 | if sort_by != "": 26 | params["sort_by"] = sort_by 27 | 28 | products_resp = requests.get(PRODUCTS_URL, params=params) 29 | if products_resp.status_code < 300: 30 | return {"products": products_resp.json()} 31 | else: 32 | return { 33 | "error": True, 34 | "error_message": products_resp.content, 35 | "error_status": products_resp.status_code, 36 | } 37 | -------------------------------------------------------------------------------- /examples/intro/product_mock/products_service.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/") 7 | async def read_root(): 8 | return {"Hello": "World"} 9 | 10 | 11 | @app.get("/products") 12 | async def read_products(category: str = None, sort_by: str = None): 13 | products = [ 14 | {"name": "Product A", "price": 10.99, "category": "Books"}, 15 | {"name": "Product B", "price": 5.99, "category": "Movies"}, 16 | {"name": "Product C", "price": 7.99, "category": "Electronics"}, 17 | {"name": "Product D", "price": 12.99, "category": "Books"}, 18 | {"name": "Product E", "price": 8.99, "category": "Movies"}, 19 | {"name": "Product F", "price": 15.99, "category": "Electronics"}, 20 | ] 21 | 22 | if category: 23 | products = [p for p in products if p["category"] == category] 24 | 25 | if sort_by: 26 | products = sorted(products, key=lambda p: p[sort_by]) 27 | 28 | return {"products": products} 29 | -------------------------------------------------------------------------------- /examples/intro/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "product-mock" 3 | version = "0.1.0" 4 | description = "An exmaple micro service that demonstrates the utility of python-wiremock" 5 | authors = ["Mike Waites "] 6 | license = "OSI Approved :: Apache Software License" 7 | readme = "README.md" 8 | packages = [{include = "product_mock"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.10.6" 12 | fastapi = "^0.95.1" 13 | requests = "^2.29.0" 14 | uvicorn = "^0.22.0" 15 | wiremock = { path = "../", develop=true} 16 | httpx = "^0.24.0" 17 | importlib-resources = "^5.12.0" 18 | docker = "^6.1.0" 19 | testcontainers = "^3.7.1" 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | pytest = "^7.3.1" 23 | black = "^23.3.0" 24 | isort = "^5.12.0" 25 | mypy = "^1.2.0" 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" 30 | 31 | [tool.black] 32 | exclude = ''' 33 | /( 34 | \.git 35 | | \.hg 36 | | \.mypy_cache 37 | | \.tox 38 | | \.venv 39 | | _build 40 | | buck-out 41 | | build 42 | | dist 43 | )/ 44 | ''' 45 | include = '\.pyi?' 46 | -------------------------------------------------------------------------------- /examples/intro/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/examples/intro/tests/__init__.py -------------------------------------------------------------------------------- /examples/intro/tests/test_java_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from fastapi.testclient import TestClient 5 | from wiremock.client import Mappings 6 | from wiremock.constants import Config 7 | from wiremock.server import WireMockServer 8 | 9 | from product_mock.overview_service import app 10 | 11 | from .utils import get_mappings, get_products 12 | 13 | client = TestClient(app) 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def wm_java(): 18 | with WireMockServer() as _wm: 19 | Config.base_url = f"http://localhost:{_wm.port}/__admin" 20 | os.environ["PRODUCTS_SERVICE_HOST"] = f"http://localhost:{_wm.port}" 21 | [Mappings.create_mapping(mapping=mapping) for mapping in get_mappings()] 22 | 23 | yield _wm 24 | 25 | Mappings.delete_all_mappings() 26 | 27 | 28 | def test_get_overview_default(wm_java): 29 | resp = client.get("/overview") 30 | 31 | assert resp.status_code == 200 32 | assert resp.json() == {"products": get_products()} 33 | 34 | 35 | def test_get_overview_with_filters(wm_java): 36 | resp = client.get("/overview?category=Books") 37 | 38 | assert resp.status_code == 200 39 | assert resp.json() == { 40 | "products": list(filter(lambda p: p["category"] == "Books", get_products())) 41 | } 42 | -------------------------------------------------------------------------------- /examples/intro/tests/test_testcontainers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from fastapi.testclient import TestClient 5 | from wiremock.client import Mappings 6 | from wiremock.constants import Config 7 | from wiremock.testing.testcontainer import wiremock_container 8 | 9 | from product_mock.overview_service import app 10 | 11 | from .utils import get_mappings, get_products 12 | 13 | client = TestClient(app) 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def wm_docker(): 18 | with wiremock_container(verify_ssl_certs=False, secure=False) as wm: 19 | 20 | Config.base_url = wm.get_url("__admin") 21 | 22 | os.environ["PRODUCTS_SERVICE_HOST"] = wm.get_base_url() 23 | 24 | [Mappings.create_mapping(mapping=mapping) for mapping in get_mappings()] 25 | 26 | yield wm 27 | 28 | Mappings.delete_all_mappings() 29 | 30 | 31 | @pytest.mark.usefixtures("wm_docker") 32 | def test_get_overview_default(): 33 | resp = client.get("/overview") 34 | 35 | assert resp.status_code == 200 36 | assert resp.json() == {"products": get_products()} 37 | 38 | 39 | @pytest.mark.usefixtures("wm_docker") 40 | def test_get_overview_with_filters(): 41 | resp = client.get("/overview?category=Books") 42 | 43 | assert resp.status_code == 200 44 | assert resp.json() == { 45 | "products": list(filter(lambda p: p["category"] == "Books", get_products())) 46 | } 47 | -------------------------------------------------------------------------------- /examples/intro/tests/utils.py: -------------------------------------------------------------------------------- 1 | from wiremock.client import HttpMethods, Mapping, MappingRequest, MappingResponse 2 | 3 | 4 | def get_products(): 5 | return [ 6 | {"name": "Mock Product A", "price": 10.99, "category": "Books"}, 7 | {"name": "Mock Product B", "price": 5.99, "category": "Movies"}, 8 | {"name": "Mock Product C", "price": 7.99, "category": "Electronics"}, 9 | {"name": "Mock Product D", "price": 12.99, "category": "Books"}, 10 | {"name": "Mock Product E", "price": 8.99, "category": "Movies"}, 11 | {"name": "Mock Product F", "price": 15.99, "category": "Electronics"}, 12 | ] 13 | 14 | 15 | def get_mappings() -> list[Mapping]: 16 | return [ 17 | Mapping( 18 | priority=100, 19 | request=MappingRequest(method=HttpMethods.GET, url="/products"), 20 | response=MappingResponse(status=200, json_body=get_products()), 21 | persistent=False, 22 | ), 23 | Mapping( 24 | priority=100, 25 | request=MappingRequest( 26 | method=HttpMethods.GET, 27 | url=r"/products?category=Books", 28 | query_parameters={"category": {"equalTo": "Books"}}, 29 | ), 30 | response=MappingResponse( 31 | status=200, 32 | json_body=list( 33 | filter(lambda p: p["category"] == "Books", get_products()) 34 | ), 35 | ), 36 | persistent=False, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /examples/quickstart/README.md: -------------------------------------------------------------------------------- 1 | # Python WireMock - Quickstart 2 | 3 | This example shows using WireMock to mock your services is by using the provided `WireMockContainer` 4 | that uses [testcontainers-python](https://github.com/testcontainers/testcontainers-python) 5 | and provisions WireMock as a test container on-demand. 6 | 7 | See the step-by-step guide [here](../../docs/quickstart.md) 8 | 9 | ## Prerequisites 10 | 11 | - Python 3.7 or above 12 | - Pip 20.0.0 or above (use `apt install python3-pip`, for example) 13 | - Pytest 7.3.0 or above (use `pip install pytest`) 14 | - Testcontainers 3.5.0 or above (use `pip install testcontainers`) 15 | 16 | ## TL;DR 17 | 18 | ```bash 19 | pip install wiremock 20 | pytest test.py -v 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/quickstart/test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | from wiremock.testing.testcontainer import wiremock_container 5 | from wiremock.constants import Config 6 | from wiremock.client import * 7 | 8 | @pytest.fixture # (1) 9 | def wiremock_server(): 10 | with wiremock_container(secure=False) as wm: 11 | Config.base_url = wm.get_url("__admin") # (2) 12 | Mappings.create_mapping( 13 | Mapping( 14 | request=MappingRequest(method=HttpMethods.GET, url="/hello"), 15 | response=MappingResponse(status=200, body="hello"), 16 | persistent=False, 17 | ) 18 | ) # (3) 19 | yield wm 20 | 21 | def test_get_hello_world(wiremock_server): # (4) 22 | response = requests.get(wiremock_server.get_url("/hello")) 23 | 24 | assert response.status_code == 200 25 | assert response.content == b"hello" 26 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: python-wiremock docs 2 | theme: 3 | name: readthedocs 4 | highlightjs: true 5 | plugins: 6 | - search 7 | markdown_extensions: 8 | - markdown_include.include: 9 | base_path: . 10 | - admonition 11 | 12 | nav: 13 | - Home: index.md 14 | - Quickstart Guide: quickstart.md 15 | - Installation: install.md 16 | - Testcontainers: testcontainers.md 17 | - Standalone: api-client.md 18 | - Contributing: CONTRIBUTING.md 19 | - Resources: 20 | - Examples: examples.md 21 | - Changelog: changelog.md 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wiremock" 3 | version = "2.6.1" 4 | description = "Wiremock Admin API Client" 5 | authors = ["Cody Lee ", "Mike Waites "] 6 | license = "OSI Approved :: Apache Software License" 7 | packages = [{include = "wiremock"}] 8 | readme = "README.md" 9 | classifiers=[ 10 | "Development Status :: 5 - Production/Stable", 11 | "Environment :: Web Environment", 12 | "Environment :: Other Environment", 13 | "Environment :: Plugins", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: Apache Software License", 16 | "Operating System :: OS Independent", 17 | "Natural Language :: English", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: Implementation", 24 | "Topic :: Internet :: WWW/HTTP", 25 | "Topic :: Software Development :: Testing", 26 | "Topic :: Software Development :: Testing :: Unit", 27 | "Topic :: Software Development :: Testing :: Mocking", 28 | "Topic :: Software Development :: Quality Assurance", 29 | "Topic :: Software Development :: Libraries :: Python Modules", 30 | ] 31 | 32 | [tool.poetry.dependencies] 33 | python = "^3.7 | ^3.8 | ^3.9 | ^3.10 | ^3.11" 34 | requests = "^2.20.0" 35 | importlib-resources = "^5.12.0" 36 | docker = {version = "^6.1.0", optional = true} 37 | testcontainers = {version = "^3.7.1", optional = true} 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | black = "^23.3.0" 41 | coverage = "^7.2.3" 42 | pytest-coverage = "^0.0" 43 | python-coveralls = "^2.9.3" 44 | responses = "^0.23.1" 45 | tox = "^4.4.12" 46 | watchdog = "^3.0.0" 47 | wheel = "^0.40.0" 48 | pytest = "^7.3.1" 49 | 50 | [tool.poetry.group.docs.dependencies] 51 | mkdocs = "^1.3.0" 52 | markdown_include = "^0.8.1" 53 | 54 | [tool.poetry.extras] 55 | testing = ["docker", "testcontainers"] 56 | 57 | [tool.pytest.ini_options] 58 | markers = [ 59 | "unit: marks tests as unit tests", 60 | "mappings", 61 | "nearmisses", 62 | "resource", 63 | "serialization", 64 | ] 65 | 66 | [build-system] 67 | requires = ["poetry-core"] 68 | build-backend = "poetry.core.masonry.api" 69 | 70 | 71 | [tool.black] 72 | exclude = ''' 73 | /( 74 | \.git 75 | | \.hg 76 | | \.mypy_cache 77 | | \.tox 78 | | \.venv 79 | | _build 80 | | buck-out 81 | | build 82 | | dist 83 | )/ 84 | ''' 85 | include = '\.pyi?$' 86 | line-length = 88 87 | -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | black==19.10b0 2 | coverage==5.0.3 3 | docutils==0.16 4 | mock==4.0.1 5 | nose==1.3.7 6 | python-coveralls==2.9.3 7 | requests==2.23.0 8 | responses==0.10.9 9 | Sphinx==2.4.3 10 | sphinx-rtd-theme==0.4.3 11 | toml==0.10.0 12 | tox==3.14.5 13 | twine==3.1.1 14 | virtualenv==20.0.5 15 | watchdog==0.10.2 16 | mkdocs==1.3.0 17 | markdown_include==0.8.1 18 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -Rf build/ dist/ wiremock.egg-info coverage/ wiremock/tests/coverage/ html/ || true 4 | poetry run pytest --cov=wiremock --tb=short 5 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wiremock.base import RestClient 4 | from wiremock.constants import Config 5 | 6 | 7 | @pytest.fixture 8 | def client(): 9 | Config.base_url = "http://localhost/__admin" 10 | Config.timeout = 1 11 | return RestClient() 12 | -------------------------------------------------------------------------------- /tests/test_containers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import cast 3 | from unittest.mock import MagicMock, Mock, patch 4 | 5 | import pytest 6 | import requests 7 | from docker.models.containers import Container 8 | 9 | from wiremock.client import ( 10 | HttpMethods, 11 | Mapping, 12 | MappingRequest, 13 | MappingResponse, 14 | Mappings, 15 | ) 16 | from wiremock.constants import Config 17 | from wiremock.testing.testcontainer import ( 18 | WireMockContainer, 19 | WireMockContainerException, 20 | wiremock_container, 21 | ) 22 | 23 | 24 | @patch.object(WireMockContainer, "get_exposed_port") 25 | @patch.object(WireMockContainer, "get_container_host_ip") 26 | def test_get_secure_base_url(mock_get_ip, mock_get_port): 27 | # Arrange 28 | wm = WireMockContainer( 29 | secure=True, 30 | verify_ssl_certs=False, 31 | ) 32 | mock_get_ip.return_value = "127.0.0.1" 33 | mock_get_port.return_value = 63379 34 | expected_url = "https://127.0.0.1:63379" 35 | 36 | # Act/Assert 37 | assert wm.get_base_url() == expected_url 38 | 39 | 40 | @patch.object(WireMockContainer, "get_exposed_port") 41 | @patch.object(WireMockContainer, "get_container_host_ip") 42 | def test_get_secure_url(mock_get_ip, mock_get_port): 43 | # Arrange 44 | wm = WireMockContainer( 45 | secure=True, 46 | verify_ssl_certs=False, 47 | ) 48 | path = "example-path" 49 | mock_get_ip.return_value = "127.0.0.1" 50 | mock_get_port.return_value = 63379 51 | 52 | expected_url = "https://127.0.0.1:63379/example-path" 53 | 54 | # Act/Assert 55 | assert wm.get_url(path) == expected_url 56 | 57 | 58 | @patch.object(WireMockContainer, "get_exposed_port") 59 | @patch.object(WireMockContainer, "get_container_host_ip") 60 | def test_get_non_secure_base_url(mock_get_ip, mock_get_port): 61 | # Arrange 62 | wm = WireMockContainer( 63 | secure=False, 64 | verify_ssl_certs=False, 65 | ) 66 | mock_get_ip.return_value = "127.0.0.1" 67 | mock_get_port.return_value = 63379 68 | expected_url = "http://127.0.0.1:63379" 69 | 70 | # Act/Assert 71 | assert wm.get_base_url() == expected_url 72 | 73 | 74 | @patch.object(WireMockContainer, "get_exposed_port") 75 | @patch.object(WireMockContainer, "get_container_host_ip") 76 | def test_get_non_secure_url(mock_get_ip, mock_get_port): 77 | # Arrange 78 | wm = WireMockContainer( 79 | secure=False, 80 | verify_ssl_certs=False, 81 | ) 82 | path = "example-path" 83 | mock_get_ip.return_value = "127.0.0.1" 84 | mock_get_port.return_value = 63379 85 | expected_url = "http://127.0.0.1:63379/example-path" 86 | 87 | # Act/Assert 88 | assert wm.get_url(path) == expected_url 89 | 90 | 91 | @patch.object(WireMockContainer, "initialize") 92 | def test_initialize_method_call_on_instance_creation(mock_init): 93 | 94 | # Arrange/Act 95 | WireMockContainer() 96 | 97 | # Assert 98 | mock_init.assert_called_once_with() 99 | 100 | 101 | @patch.object(WireMockContainer, "with_https_port") 102 | def test_initialize_set_defaults(mock_set_secure_port): 103 | 104 | wm = WireMockContainer() 105 | 106 | assert wm.https_server_port == 8443 107 | assert wm.http_server_port == 8080 108 | assert wm.wire_mock_args == [] 109 | assert wm.mapping_stubs == {} 110 | assert wm.mapping_files == {} 111 | assert wm.extensions == {} 112 | mock_set_secure_port.assert_called_once_with() 113 | 114 | 115 | @patch.object(WireMockContainer, "with_http_port") 116 | @patch.object(WireMockContainer, "with_https_port") 117 | def test_initialize_non_secure_mode_sets_http_port( 118 | mock_set_secure_port, mock_set_unsecure_port 119 | ): 120 | wm = WireMockContainer(secure=False) 121 | 122 | assert wm.https_server_port == 8443 123 | assert wm.http_server_port == 8080 124 | assert wm.wire_mock_args == [] 125 | assert wm.mapping_stubs == {} 126 | assert wm.mapping_files == {} 127 | assert wm.extensions == {} 128 | 129 | assert not mock_set_secure_port.called 130 | mock_set_unsecure_port.assert_called_once_with() 131 | 132 | 133 | @patch.object(WireMockContainer, "with_exposed_ports") 134 | @patch.object(WireMockContainer, "with_cli_arg") 135 | def test_with_https_port_default(mock_cli_arg, mock_expose_port): 136 | 137 | # Arrange 138 | wm = WireMockContainer(init=False) 139 | 140 | # Act 141 | wm.with_https_port() 142 | 143 | # Assert 144 | mock_cli_arg.assert_called_once_with("--https-port", "8443") 145 | mock_expose_port.assert_called_once_with(wm.https_server_port) 146 | 147 | 148 | @patch.object(WireMockContainer, "with_exposed_ports") 149 | @patch.object(WireMockContainer, "with_cli_arg") 150 | def test_with_https_port_with_user_defined_port_value(mock_cli_arg, mock_expose_port): 151 | 152 | # Arrange 153 | wm = WireMockContainer(https_server_port=9443, init=False) 154 | 155 | # Act 156 | wm.with_https_port() 157 | 158 | # Assert 159 | mock_cli_arg.assert_called_once_with("--https-port", "9443") 160 | mock_expose_port.assert_called_once_with(9443) 161 | 162 | 163 | @patch.object(WireMockContainer, "with_exposed_ports") 164 | @patch.object(WireMockContainer, "with_cli_arg") 165 | def test_with_http_port_default(mock_cli_arg, mock_expose_port): 166 | 167 | # Arrange 168 | wm = WireMockContainer(init=False) 169 | 170 | # Act 171 | wm.with_http_port() 172 | 173 | # Assert 174 | mock_cli_arg.assert_called_once_with("--port", "8080") 175 | mock_expose_port.assert_called_once_with(wm.http_server_port) 176 | 177 | 178 | @patch.object(WireMockContainer, "with_exposed_ports") 179 | @patch.object(WireMockContainer, "with_cli_arg") 180 | def test_with_http_port_with_user_defined_port_value(mock_cli_arg, mock_expose_port): 181 | 182 | # Arrange 183 | wm = WireMockContainer(http_server_port=5000, init=False) 184 | 185 | # Act 186 | wm.with_http_port() 187 | 188 | # Assert 189 | mock_cli_arg.assert_called_once_with("--port", "5000") 190 | mock_expose_port.assert_called_once_with(5000) 191 | 192 | 193 | @patch("wiremock.testing.testcontainer.requests.get") 194 | @patch.object(WireMockContainer, "get_url") 195 | def test_container_starts_with_custom_https_port(mock_get_url, mock_get): 196 | 197 | # Arrange 198 | mock_get_url.return_value = "http://localhost/__admin/mappings" 199 | resp_mock = MagicMock(spec=requests.Response) 200 | resp_mock.status_code = 200 201 | mock_get.return_value = resp_mock 202 | wm = WireMockContainer(verify_ssl_certs=False, https_server_port=9443) 203 | 204 | # Act 205 | assert wm.server_running() is True 206 | 207 | 208 | @patch("wiremock.testing.testcontainer.requests.get") 209 | @patch.object(WireMockContainer, "get_url") 210 | def test_container_starts_with_custom_http_port(mock_get_url, mock_get): 211 | 212 | # Arrange 213 | mock_get_url.return_value = "http://localhost/__admin/mappings" 214 | resp_mock = MagicMock(spec=requests.Response) 215 | resp_mock.status_code = 200 216 | mock_get.return_value = resp_mock 217 | wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) 218 | 219 | # Act 220 | 221 | assert wm.server_running() is True 222 | 223 | 224 | @patch("wiremock.testing.testcontainer.requests.get") 225 | @patch.object(WireMockContainer, "get_url") 226 | def test_container_not_running_returns_false(mock_get_url, mock_get): 227 | 228 | # Arrange 229 | mock_get_url.return_value = "http://localhost/__admin/mappings" 230 | resp_mock = MagicMock(spec=requests.Response) 231 | resp_mock.status_code = 403 232 | mock_get.return_value = resp_mock 233 | wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) 234 | 235 | # Act 236 | 237 | assert wm.server_running() is False 238 | 239 | 240 | @patch("wiremock.testing.testcontainer.requests.post") 241 | @patch.object(WireMockContainer, "get_url") 242 | def test_reload_mappings(mock_get_url, mock_post): 243 | 244 | # Arrange 245 | mock_get_url.return_value = "http://localhost/__admin/mappings" 246 | resp_mock = MagicMock(spec=requests.Response) 247 | resp_mock.status_code = 200 248 | mock_post.return_value = resp_mock 249 | wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) 250 | 251 | # Act 252 | resp = wm.reload_mappings() 253 | 254 | assert resp.status_code == 200 255 | 256 | 257 | @patch("wiremock.testing.testcontainer.requests.post") 258 | @patch.object(WireMockContainer, "get_url") 259 | def test_reload_mappings_failure_raises_exception(mock_get_url, mock_post): 260 | 261 | # Arrange 262 | mock_get_url.return_value = "http://localhost/__admin/mappings" 263 | resp_mock = MagicMock(spec=requests.Response) 264 | resp_mock.status_code = 403 265 | mock_post.return_value = resp_mock 266 | wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) 267 | 268 | # Act 269 | with pytest.raises(WireMockContainerException): 270 | wm.reload_mappings() 271 | 272 | 273 | def test_container_with_cli_arg_sets_cmd_line_args(): 274 | 275 | # Arrange 276 | wm = WireMockContainer() 277 | 278 | # Act 279 | wm.with_cli_arg("--foo", "bar") 280 | 281 | # Assert 282 | assert wm.wire_mock_args == ["--https-port", "8443", "--foo", "bar"] 283 | 284 | 285 | def test_container_with_command_generates_command_from_cli_args(): 286 | 287 | # Arrange 288 | wm = WireMockContainer() 289 | 290 | # Act 291 | wm.with_command() 292 | 293 | # Assert 294 | assert wm._command == "--https-port 8443" 295 | 296 | 297 | def test_container_with_command_override(): 298 | 299 | # Arrange 300 | wm = WireMockContainer() 301 | 302 | # Act 303 | wm.with_command(cmd="--foo bar") 304 | 305 | # Assert 306 | assert wm._command == "--foo bar" 307 | 308 | 309 | @patch.object(WireMockContainer, "get_wrapped_container", spec=Container) 310 | def test_copy_file_to_container(mock_get_container: Mock, tmp_path: Path): 311 | 312 | # Arrange 313 | d = tmp_path / "mappings" 314 | d.mkdir() 315 | mapping = d / "mapping.json" 316 | mapping.write_text('{"foo": "bar"}') 317 | wm = WireMockContainer() 318 | 319 | # Act 320 | wm.copy_file_to_container(mapping, Path(wm.MAPPINGS_DIR)) 321 | 322 | # Assert 323 | mock_get_container.return_value.put_archive.assert_called_once_with( 324 | path=Path(wm.MAPPINGS_DIR).as_posix(), data=b'{"foo": "bar"}' 325 | ) 326 | 327 | 328 | @pytest.mark.container_test 329 | def test_configure_manually(): 330 | 331 | wm = cast( 332 | WireMockContainer, 333 | ( 334 | WireMockContainer(verify_ssl_certs=False) 335 | .with_mapping( 336 | "hello-world.json", 337 | { 338 | "request": {"method": "GET", "url": "/hello"}, 339 | "response": {"status": 200, "body": "hello"}, 340 | }, 341 | ) 342 | .with_mapping( 343 | "hello-world-file.json", 344 | { 345 | "request": {"method": "GET", "url": "/hello2"}, 346 | "response": {"status": 200, "bodyFileName": "hello.json"}, 347 | }, 348 | ) 349 | .with_file("hello.json", {"message": "Hello World !"}) 350 | .with_cli_arg("--verbose", "") 351 | .with_cli_arg("--root-dir", "/home/wiremock") 352 | .with_env("JAVA_OPTS", "-Djava.net.preferIPv4Stack=true") 353 | ), 354 | ) 355 | with wm: 356 | resp1 = requests.get(wm.get_url("/hello"), verify=False) 357 | resp2 = requests.get(wm.get_url("/hello2"), verify=False) 358 | assert resp1.status_code == 200 359 | assert resp1.content == b"hello" 360 | assert resp2.status_code == 200 361 | assert resp2.content == b'{"message": "Hello World !"}' 362 | 363 | 364 | @pytest.mark.container_test 365 | def test_configure_via_wiremock_container_context_manager(): 366 | 367 | mappings = [ 368 | ( 369 | "hello-world.json", 370 | { 371 | "request": {"method": "GET", "url": "/hello"}, 372 | "response": {"status": 200, "body": "hello"}, 373 | }, 374 | ) 375 | ] 376 | 377 | with wiremock_container(mappings=mappings, verify_ssl_certs=False) as wm: 378 | 379 | resp1 = requests.get(wm.get_url("/hello"), verify=False) 380 | assert resp1.status_code == 200 381 | assert resp1.content == b"hello" 382 | 383 | 384 | @pytest.mark.container_test 385 | def test_container_sdk_integration(): 386 | 387 | with wiremock_container(secure=False) as wm: 388 | 389 | Config.base_url = wm.get_url("__admin") 390 | 391 | Mappings.create_mapping( 392 | Mapping( 393 | priority=100, 394 | request=MappingRequest(method=HttpMethods.GET, url="/hello"), 395 | response=MappingResponse(status=200, body="hello"), 396 | persistent=False, 397 | ) 398 | ) 399 | 400 | resp1 = requests.get(wm.get_url("/hello"), verify=False) 401 | assert resp1.status_code == 200 402 | assert resp1.content == b"hello" 403 | -------------------------------------------------------------------------------- /tests/test_resources/test_base_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from requests import Response 3 | 4 | from wiremock.exceptions import UnexpectedResponseException 5 | 6 | 7 | # Helpers 8 | def create_dummy_response(status_code=200): 9 | resp = Response() 10 | resp.status_code = status_code 11 | return resp 12 | 13 | 14 | def test_handle_response(client): 15 | for status_code in [200, 201, 204]: 16 | resp = create_dummy_response(status_code) 17 | returned = client.handle_response(resp) 18 | assert returned == resp 19 | 20 | resp = create_dummy_response(203) 21 | with pytest.raises(UnexpectedResponseException): 22 | client.handle_response(resp) 23 | -------------------------------------------------------------------------------- /tests/test_resources/test_mapping/test_mapping_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import responses 3 | 4 | from wiremock.client import ( 5 | AllMappings, 6 | Mapping, 7 | MappingMeta, 8 | MappingRequest, 9 | MappingResponse, 10 | Mappings, 11 | ) 12 | 13 | 14 | @pytest.mark.unit 15 | @pytest.mark.mappings 16 | @pytest.mark.resource 17 | @responses.activate 18 | def test_create_mapping(): 19 | m = Mapping( 20 | priority=1, 21 | request=MappingRequest(url="test", method="GET"), 22 | response=MappingResponse(status=200, body="test"), 23 | ) 24 | 25 | e = Mapping(**m, id="1234-5678") 26 | resp = e.get_json_data() 27 | responses.add( 28 | responses.POST, "http://localhost/__admin/mappings", json=resp, status=200 29 | ) 30 | 31 | r = Mappings.create_mapping(m) 32 | assert isinstance(r, Mapping) 33 | assert r.id == "1234-5678" 34 | assert r.priority == 1 35 | assert r.request == m.request 36 | assert r.response == m.response 37 | 38 | 39 | @pytest.mark.unit 40 | @pytest.mark.mappings 41 | @pytest.mark.resource 42 | @responses.activate 43 | def test_retrieve_all_mappings(): 44 | e = AllMappings( 45 | mappings=[ 46 | Mapping(id="1234-5678", priority=1), 47 | ], 48 | meta=MappingMeta(total=1), 49 | ) 50 | resp = e.get_json_data() 51 | responses.add( 52 | responses.GET, 53 | "http://localhost/__admin/mappings", 54 | json=resp, 55 | status=200, 56 | ) 57 | 58 | r = Mappings.retrieve_all_mappings() 59 | assert isinstance(r, AllMappings) 60 | assert isinstance(r.meta, MappingMeta) 61 | assert 1 == r.meta.total 62 | 63 | 64 | @pytest.mark.unit 65 | @pytest.mark.mappings 66 | @pytest.mark.resource 67 | @responses.activate 68 | def test_retrieve_mapping(): 69 | e = Mapping(id="1234-5678", priority=1) 70 | resp = e.get_json_data() 71 | responses.add( 72 | responses.GET, 73 | "http://localhost/__admin/mappings/1234-5678", 74 | json=resp, 75 | status=200, 76 | ) 77 | 78 | r = Mappings.retrieve_mapping(e) 79 | assert isinstance(r, Mapping) 80 | assert "1234-5678" == r.id 81 | assert 1 == r.priority 82 | 83 | 84 | @pytest.mark.unit 85 | @pytest.mark.mappings 86 | @pytest.mark.resource 87 | @responses.activate 88 | def test_update_mapping(): 89 | e = Mapping(id="1234-5678", priority=1) 90 | resp = e.get_json_data() 91 | responses.add( 92 | responses.PUT, 93 | "http://localhost/__admin/mappings/1234-5678", 94 | json=resp, 95 | status=200, 96 | ) 97 | 98 | r = Mappings.update_mapping(e) 99 | assert isinstance(r, Mapping) 100 | assert "1234-5678" == r.id 101 | assert 1 == r.priority 102 | 103 | 104 | @pytest.mark.unit 105 | @pytest.mark.mappings 106 | @pytest.mark.resource 107 | @responses.activate 108 | def test_persist_mappings(): 109 | responses.add( 110 | responses.POST, 111 | "http://localhost/__admin/mappings/save", 112 | body="", 113 | status=200, 114 | ) 115 | 116 | r = Mappings.persist_mappings() 117 | assert r.status_code == 200 118 | 119 | 120 | @pytest.mark.unit 121 | @pytest.mark.mappings 122 | @pytest.mark.resource 123 | @responses.activate 124 | def test_reset_mappings(): 125 | responses.add( 126 | responses.POST, 127 | "http://localhost/__admin/mappings/reset", 128 | body="", 129 | status=200, 130 | ) 131 | 132 | r = Mappings.reset_mappings() 133 | assert r.status_code == 200 134 | 135 | 136 | @pytest.mark.unit 137 | @pytest.mark.mappings 138 | @pytest.mark.resource 139 | @responses.activate 140 | def test_delete_all_mappings(): 141 | responses.add( 142 | responses.DELETE, 143 | "http://localhost/__admin/mappings", 144 | body="", 145 | status=200, 146 | ) 147 | 148 | r = Mappings.delete_all_mappings() 149 | assert r.status_code == 200 150 | 151 | 152 | @pytest.mark.unit 153 | @pytest.mark.mappings 154 | @pytest.mark.resource 155 | @responses.activate 156 | def test_delete_mapping(): 157 | e = Mapping(id="1234-5678", priority=1) 158 | responses.add( 159 | responses.DELETE, 160 | "http://localhost/__admin/mappings/1234-5678", 161 | body="", 162 | status=200, 163 | ) 164 | 165 | r = Mappings.delete_mapping(e) 166 | assert r.status_code == 200 167 | 168 | 169 | @pytest.mark.unit 170 | @pytest.mark.mappings 171 | @pytest.mark.resource 172 | @responses.activate 173 | def test_delete_mapping_by_metadata(): 174 | responses.add( 175 | responses.POST, 176 | "http://localhost/__admin/mappings/remove-by-metadata", 177 | body="{}", 178 | status=200, 179 | ) 180 | 181 | r = Mappings.delete_mapping_by_metadata( 182 | { 183 | "matchesJsonPath": { 184 | "expression": "$.some.key", 185 | "equalTo": "SomeValue", 186 | }, 187 | } 188 | ) 189 | 190 | assert r.status_code == 200 191 | -------------------------------------------------------------------------------- /tests/test_resources/test_mapping/test_mapping_serialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.utils import assertDictContainsKeyWithValue 4 | from wiremock.resources.mappings import ( 5 | AllMappings, 6 | BasicAuthCredentials, 7 | DelayDistribution, 8 | DelayDistributionMethods, 9 | Mapping, 10 | MappingMeta, 11 | MappingRequest, 12 | MappingResponse, 13 | ) 14 | 15 | 16 | @pytest.mark.unit 17 | @pytest.mark.serialization 18 | @pytest.mark.mappings 19 | def test_basic_auth_credentials_serialization(): 20 | e = BasicAuthCredentials(username="username", password="password") 21 | serialized = e.get_json_data() 22 | assert serialized["username"] == "username" 23 | assert serialized["password"] == "password" 24 | 25 | 26 | @pytest.mark.unit 27 | @pytest.mark.serialization 28 | @pytest.mark.mappings 29 | def test_basic_auth_credentials_deserialization(): 30 | serialized = {"username": "username", "password": "password"} 31 | e = BasicAuthCredentials.from_dict(serialized) 32 | assert isinstance(e, BasicAuthCredentials) 33 | assert e.username == "username" 34 | assert e.password == "password" 35 | 36 | 37 | @pytest.mark.unit 38 | @pytest.mark.serialization 39 | @pytest.mark.mappings 40 | def test_mapping_meta_serialization(): 41 | e = MappingMeta(total=1) 42 | serialized = e.get_json_data() 43 | assert serialized["total"] == 1 44 | 45 | 46 | @pytest.mark.unit 47 | @pytest.mark.serialization 48 | @pytest.mark.mappings 49 | def test_mapping_meta_deserialization(): 50 | serialized = {"total": 1} 51 | e = MappingMeta.from_dict(serialized) 52 | assert isinstance(e, MappingMeta) 53 | assert e.total == 1 54 | 55 | 56 | @pytest.mark.unit 57 | @pytest.mark.serialization 58 | @pytest.mark.mappings 59 | def test_delay_distribution_serialization(): 60 | e = DelayDistribution( 61 | distribution_type=DelayDistributionMethods.LOG_NORMAL, 62 | median=0.1, 63 | sigma=0.2, 64 | upper=4, 65 | lower=3, 66 | ) 67 | serialized = e.get_json_data() 68 | assert serialized["type"] == "lognormal" 69 | assert serialized["median"] == 0.1 70 | assert serialized["sigma"] == 0.2 71 | assert serialized["lower"] == 3 72 | assert serialized["upper"] == 4 73 | 74 | 75 | @pytest.mark.unit 76 | @pytest.mark.serialization 77 | @pytest.mark.mappings 78 | def test_delay_distribution_deserialization(): 79 | serialized = { 80 | "type": "lognormal", 81 | "median": 0.1, 82 | "sigma": 0.2, 83 | "lower": 3, 84 | "upper": 4, 85 | } 86 | e = DelayDistribution.from_dict(serialized) 87 | assert isinstance(e, DelayDistribution) 88 | assert e.distribution_type == "lognormal" 89 | assert e.median == 0.1 90 | assert e.sigma == 0.2 91 | assert e.lower == 3 92 | assert e.upper == 4 93 | 94 | 95 | @pytest.mark.unit 96 | @pytest.mark.serialization 97 | @pytest.mark.mappings 98 | def test_mapping_request_serialization(): 99 | e = MappingRequest( 100 | method="GET", 101 | url="test1", 102 | url_path="test2", 103 | url_path_pattern="test3", 104 | url_pattern="test4", 105 | basic_auth_credentials=BasicAuthCredentials( 106 | username="username", password="password" 107 | ), 108 | cookies={"chocolate": "chip"}, 109 | headers={"Accept": "stuff"}, 110 | query_parameters={"param": "1"}, 111 | body_patterns={"test": "test2"}, 112 | metadata={"key": "value"}, 113 | ) 114 | serialized = e.get_json_data() 115 | assert serialized["method"] == "GET" 116 | assert serialized["url"] == "test1" 117 | assert serialized["urlPath"] == "test2" 118 | assert serialized["urlPathPattern"] == "test3" 119 | assert serialized["urlPattern"] == "test4" 120 | assert serialized["basicAuthCredentials"] == { 121 | "username": "username", 122 | "password": "password", 123 | } 124 | assert serialized["cookies"] == {"chocolate": "chip"} 125 | assert serialized["headers"] == {"Accept": "stuff"} 126 | 127 | assert serialized["queryParameters"] == {"param": "1"} 128 | assert serialized["bodyPatterns"] == {"test": "test2"} 129 | assert serialized["metadata"] == {"key": "value"} 130 | 131 | 132 | @pytest.mark.unit 133 | @pytest.mark.serialization 134 | @pytest.mark.mappings 135 | def test_mapping_request_deserialization(): 136 | serialized = { 137 | "method": "GET", 138 | "url": "test1", 139 | "urlPath": "test2", 140 | "urlPathPattern": "test3", 141 | "urlPattern": "test4", 142 | "basicAuthCredentials": { 143 | "username": "username", 144 | "password": "password", 145 | }, 146 | "cookies": {"chocolate": "chip"}, 147 | "headers": {"Accept": "stuff"}, 148 | "queryParameters": {"param": "1"}, 149 | "bodyPatterns": {"test": "test2"}, 150 | "metadata": {"key": [1, 2, 3]}, 151 | } 152 | e = MappingRequest.from_dict(serialized) 153 | assert isinstance(e, MappingRequest) 154 | assert "GET" == e.method 155 | assert "test1" == e.url 156 | assert "test2" == e.url_path 157 | assert "test3" == e.url_path_pattern 158 | assert "test4" == e.url_pattern 159 | assert isinstance(e.basic_auth_credentials, BasicAuthCredentials) 160 | assert "username" == e.basic_auth_credentials.username 161 | assert "password" == e.basic_auth_credentials.password 162 | assert {"chocolate": "chip"} == e.cookies 163 | assert {"Accept": "stuff"} == e.headers 164 | assert {"param": "1"} == e.query_parameters 165 | assert {"test": "test2"} == e.body_patterns 166 | assert {"key": [1, 2, 3]} == e.metadata 167 | 168 | 169 | @pytest.mark.unit 170 | @pytest.mark.serialization 171 | @pytest.mark.mappings 172 | def test_mapping_response_serialization(): 173 | e = MappingResponse( 174 | additional_proxy_request_headers={"test": "1"}, 175 | base64_body="test2", 176 | body="test3", 177 | body_file_name="test4", 178 | json_body="test5", 179 | delay_distribution=DelayDistribution( 180 | distribution_type="lognormal", sigma=0.1, median=0.2 181 | ), 182 | fault="test6", 183 | fixed_delay_milliseconds=500, 184 | from_configured_stub="test7", 185 | headers={"test": "1"}, 186 | proxy_base_url="test8", 187 | status=200, 188 | status_message="test9", 189 | transformer_parameters={"test2": "2"}, 190 | transformers=["test10"], 191 | ) 192 | serialized = e.get_json_data() 193 | assertDictContainsKeyWithValue( 194 | serialized, "additionalProxyRequestHeaders", {"test": "1"} 195 | ) 196 | assertDictContainsKeyWithValue(serialized, "base64Body", "test2") 197 | assertDictContainsKeyWithValue(serialized, "body", "test3") 198 | assertDictContainsKeyWithValue(serialized, "bodyFileName", "test4") 199 | assertDictContainsKeyWithValue(serialized, "jsonBody", "test5") 200 | assertDictContainsKeyWithValue( 201 | serialized, 202 | "delayDistribution", 203 | { 204 | "type": "lognormal", 205 | "sigma": 0.1, 206 | "median": 0.2, 207 | }, 208 | ) 209 | assertDictContainsKeyWithValue(serialized, "fault", "test6") 210 | assertDictContainsKeyWithValue(serialized, "fixedDelayMilliseconds", 500) 211 | assertDictContainsKeyWithValue(serialized, "fromConfiguredStub", "test7") 212 | assertDictContainsKeyWithValue(serialized, "headers", {"test": "1"}) 213 | assertDictContainsKeyWithValue(serialized, "proxyBaseUrl", "test8") 214 | assertDictContainsKeyWithValue(serialized, "status", 200) 215 | assertDictContainsKeyWithValue(serialized, "statusMessage", "test9") 216 | assertDictContainsKeyWithValue( 217 | serialized, 218 | "transformerParameters", 219 | {"test2": "2"}, 220 | ) 221 | assertDictContainsKeyWithValue(serialized, "transformers", ["test10"]) 222 | 223 | 224 | @pytest.mark.unit 225 | @pytest.mark.serialization 226 | @pytest.mark.mappings 227 | def test_mapping_response_deserialization(): 228 | serialized = { 229 | "additionalProxyRequestHeaders": {"test": "1"}, 230 | "base64Body": "test2", 231 | "body": "test3", 232 | "bodyFileName": "test4", 233 | "jsonBody": "test5", 234 | "delayDistribution": {"type": "lognormal", "sigma": 0.1, "median": 0.2}, 235 | "fault": "test6", 236 | "fixedDelayMilliseconds": 500, 237 | "fromConfiguredStub": "test7", 238 | "headers": {"test": "1"}, 239 | "proxyBaseUrl": "test8", 240 | "status": 200, 241 | "statusMessage": "test9", 242 | "transformerParameters": {"test2": "2"}, 243 | "transformers": ["test10"], 244 | } 245 | e = MappingResponse.from_dict(serialized) 246 | assert isinstance(e, MappingResponse) 247 | assert e.additional_proxy_request_headers == {"test": "1"} 248 | assert e.base64_body == "test2" 249 | assert e.body == "test3" 250 | assert e.body_file_name == "test4" 251 | assert e.json_body == "test5" 252 | assert isinstance(e.delay_distribution, DelayDistribution) 253 | assert e.delay_distribution.distribution_type == "lognormal" 254 | assert e.fault == "test6" 255 | assert e.fixed_delay_milliseconds == 500 256 | assert e.from_configured_stub == "test7" 257 | assert e.headers == {"test": "1"} 258 | assert e.proxy_base_url == "test8" 259 | assert e.status == 200 260 | assert e.status_message == "test9" 261 | assert e.transformer_parameters == {"test2": "2"} 262 | assert e.body == "test3" 263 | assert e.body_file_name == "test4" 264 | assert e.json_body == "test5" 265 | assert isinstance(e.delay_distribution, DelayDistribution) 266 | assert e.delay_distribution.distribution_type == "lognormal" 267 | assert e.fault == "test6" 268 | assert e.fixed_delay_milliseconds == 500 269 | assert e.from_configured_stub == "test7" 270 | assert e.headers == {"test": "1"} 271 | assert e.proxy_base_url == "test8" 272 | assert e.status == 200 273 | assert e.status_message == "test9" 274 | assert e.transformer_parameters == {"test2": "2"} 275 | assert e.transformers == ["test10"] 276 | 277 | 278 | @pytest.mark.unit 279 | @pytest.mark.serialization 280 | @pytest.mark.mappings 281 | def test_mapping_serialization(): 282 | e = Mapping( 283 | priority=1, 284 | request=MappingRequest(method="GET", url="test"), 285 | response=MappingResponse(status=200, status_message="test2"), 286 | persistent=False, 287 | post_serve_actions={"test": "1"}, 288 | new_scenario_state="test3", 289 | required_scenario_state="test4", 290 | scenario_name="test5", 291 | ) 292 | serialized = e.get_json_data() 293 | 294 | assertDictContainsKeyWithValue(serialized, "priority", 1) 295 | assertDictContainsKeyWithValue( 296 | serialized, 297 | "request", 298 | { 299 | "method": "GET", 300 | "url": "test", 301 | }, 302 | ) 303 | assertDictContainsKeyWithValue( 304 | serialized, 305 | "response", 306 | { 307 | "status": 200, 308 | "statusMessage": "test2", 309 | }, 310 | ) 311 | assertDictContainsKeyWithValue(serialized, "persistent", False) 312 | 313 | assertDictContainsKeyWithValue( 314 | serialized, 315 | "postServeActions", 316 | {"test": "1"}, 317 | ) 318 | assertDictContainsKeyWithValue(serialized, "newScenarioState", "test3") 319 | assertDictContainsKeyWithValue( 320 | serialized, 321 | "requiredScenarioState", 322 | "test4", 323 | ) 324 | assertDictContainsKeyWithValue(serialized, "scenarioName", "test5") 325 | 326 | 327 | @pytest.mark.unit 328 | @pytest.mark.serialization 329 | @pytest.mark.mappings 330 | def test_mapping_deserialization(): 331 | serialized = { 332 | "priority": 1, 333 | "request": {"method": "GET", "url": "test"}, 334 | "response": {"status": 200, "statusMessage": "test2"}, 335 | "persistent": False, 336 | "postServeActions": {"test": "1"}, 337 | "newScenarioState": "test3", 338 | "requiredScenarioState": "test4", 339 | "scenarioName": "test5", 340 | } 341 | e = Mapping.from_dict(serialized) 342 | assert isinstance(e, Mapping) 343 | assert e.priority == 1 344 | assert isinstance(e.request, MappingRequest) 345 | assert e.request.method == "GET" 346 | assert e.request.url == "test" 347 | assert isinstance(e.response, MappingResponse) 348 | assert e.response.status == 200 349 | assert e.response.status_message == "test2" 350 | assert e.persistent is False 351 | assert e.post_serve_actions == {"test": "1"} 352 | assert e.new_scenario_state == "test3" 353 | assert e.required_scenario_state == "test4" 354 | assert e.scenario_name == "test5" 355 | 356 | 357 | @pytest.mark.unit 358 | @pytest.mark.serialization 359 | @pytest.mark.mappings 360 | def test_all_mappings_serialization(): 361 | e = AllMappings( 362 | mappings=[ 363 | Mapping(priority=1), 364 | ], 365 | meta=MappingMeta(total=1), 366 | ) 367 | serialized = e.get_json_data() 368 | assertDictContainsKeyWithValue( 369 | serialized, 370 | "mappings", 371 | [ 372 | {"priority": 1}, 373 | ], 374 | ) 375 | assertDictContainsKeyWithValue(serialized, "meta", {"total": 1}) 376 | 377 | 378 | @pytest.mark.unit 379 | @pytest.mark.serialization 380 | @pytest.mark.mappings 381 | def test_all_mappings_deserialization(): 382 | serialized = { 383 | "mappings": [ 384 | {"priority": 1}, 385 | ], 386 | "meta": {"total": 1}, 387 | } 388 | e = AllMappings.from_dict(serialized) 389 | assert isinstance(e, AllMappings) 390 | assert isinstance(e.mappings, list) 391 | m = e.mappings[0] 392 | assert isinstance(m, Mapping) 393 | assert m.priority == 1 394 | assert isinstance(e.meta, MappingMeta) 395 | assert e.meta.total == 1 396 | -------------------------------------------------------------------------------- /tests/test_resources/test_nearmisses/test_near_misses_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import responses 3 | 4 | from wiremock.client import ( 5 | NearMisses, 6 | NearMissMatch, 7 | NearMissMatchPatternRequest, 8 | NearMissMatchRequest, 9 | NearMissMatchResponse, 10 | NearMissMatchResult, 11 | NearMissRequestPatternResult, 12 | ) 13 | from wiremock.resources.mappings import HttpMethods 14 | 15 | 16 | @pytest.mark.unit 17 | @pytest.mark.resource 18 | @pytest.mark.nearmisses 19 | @responses.activate 20 | def test_find_nearest_misses_by_request(): 21 | e = NearMissMatchResponse( 22 | near_misses=[ 23 | NearMissMatch( 24 | request=NearMissMatchRequest(url="test", method="GET"), 25 | request_pattern=NearMissRequestPatternResult( 26 | url="test1", 27 | method="GET", 28 | ), 29 | match_result=NearMissMatchResult(distance=0.5), 30 | ), 31 | ] 32 | ) 33 | resp = e.get_json_data() 34 | responses.add( 35 | responses.POST, 36 | "http://localhost/__admin/near-misses/request", 37 | json=resp, 38 | status=200, 39 | ) 40 | 41 | near_miss_match_request = NearMissMatchRequest( 42 | url="test", 43 | method=HttpMethods.GET, 44 | ) 45 | r = NearMisses.find_nearest_misses_by_request(near_miss_match_request) 46 | assert isinstance(r, NearMissMatchResponse) 47 | assert isinstance(r.near_misses, list) 48 | result = r.near_misses[0] 49 | assert isinstance(result, NearMissMatch) 50 | assert result.request.url == "test" 51 | 52 | 53 | @pytest.mark.unit 54 | @pytest.mark.resource 55 | @pytest.mark.nearmisses 56 | @responses.activate 57 | def test_find_nearest_misses_by_request_pattern(): 58 | e = NearMissMatchResponse( 59 | near_misses=[ 60 | NearMissMatch( 61 | request=NearMissMatchRequest(url="test", method="GET"), 62 | request_pattern=NearMissRequestPatternResult( 63 | url="test1", 64 | method="GET", 65 | ), 66 | match_result=NearMissMatchResult(distance=0.5), 67 | ), 68 | ] 69 | ) 70 | resp = e.get_json_data() 71 | responses.add( 72 | responses.POST, 73 | "http://localhost/__admin/near-misses/request-pattern", 74 | json=resp, 75 | status=200, 76 | ) 77 | 78 | near_miss_match_request_pattern = NearMissMatchPatternRequest( 79 | url="test", method=HttpMethods.GET 80 | ) 81 | r = NearMisses.find_nearest_misses_by_request_pattern( 82 | near_miss_match_request_pattern 83 | ) 84 | assert isinstance(r, NearMissMatchResponse) 85 | assert isinstance(r.near_misses, list) 86 | result = r.near_misses[0] 87 | assert isinstance(result, NearMissMatch) 88 | assert result.request.url == "test" 89 | -------------------------------------------------------------------------------- /tests/test_resources/test_nearmisses/test_near_misses_serialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.utils import ( 4 | assertDictContainsKeyWithValue, 5 | assertDictContainsKeyWithValueType, 6 | ) 7 | from wiremock.resources.mappings import BasicAuthCredentials, CommonHeaders, HttpMethods 8 | from wiremock.resources.near_misses import ( 9 | NearMissMatch, 10 | NearMissMatchPatternRequest, 11 | NearMissMatchRequest, 12 | NearMissMatchResponse, 13 | NearMissMatchResult, 14 | NearMissRequestPatternResult, 15 | ) 16 | 17 | 18 | @pytest.mark.unit 19 | @pytest.mark.serialization 20 | @pytest.mark.nearmisses 21 | def test_near_miss_match_pattern_request_serialization(): 22 | e = NearMissMatchPatternRequest( 23 | url="test", 24 | url_pattern="test2", 25 | url_path="test3", 26 | url_path_pattern="test4", 27 | method=HttpMethods.GET, 28 | client_ip="1.1.1.1", 29 | headers={CommonHeaders.ACCEPT: "json"}, 30 | query_parameters={"test": 1}, 31 | cookies={"chocolate": "chip"}, 32 | body_patterns={"test": 3}, 33 | basic_auth_credentials=BasicAuthCredentials( 34 | username="username", password="password" 35 | ), 36 | browser_proxy_request=False, 37 | logged_date=12345, 38 | logged_date_string="1/1/2017 00:00:00+0000", 39 | ) 40 | serialized = e.get_json_data() 41 | assertDictContainsKeyWithValue(serialized, "url", "test") 42 | assertDictContainsKeyWithValue(serialized, "urlPattern", "test2") 43 | assertDictContainsKeyWithValue(serialized, "urlPath", "test3") 44 | assertDictContainsKeyWithValue(serialized, "urlPathPattern", "test4") 45 | assertDictContainsKeyWithValue(serialized, "method", "GET") 46 | assertDictContainsKeyWithValue(serialized, "clientIp", "1.1.1.1") 47 | assertDictContainsKeyWithValue(serialized, "headers", {"Accept": "json"}) 48 | assertDictContainsKeyWithValue(serialized, "queryParameters", {"test": 1}) 49 | assertDictContainsKeyWithValue(serialized, "cookies", {"chocolate": "chip"}) 50 | assertDictContainsKeyWithValue(serialized, "bodyPatterns", {"test": 3}) 51 | assertDictContainsKeyWithValue( 52 | serialized, 53 | "basicAuthCredentials", 54 | {"username": "username", "password": "password"}, 55 | ) 56 | assertDictContainsKeyWithValue(serialized, "browserProxyRequest", False) 57 | assertDictContainsKeyWithValue(serialized, "loggedDate", 12345) 58 | assertDictContainsKeyWithValue( 59 | serialized, "loggedDateString", "1/1/2017 00:00:00+0000" 60 | ) 61 | 62 | 63 | @pytest.mark.unit 64 | @pytest.mark.serialization 65 | @pytest.mark.nearmisses 66 | def test_near_miss_match_pattern_request_deserialization(): 67 | serialized = { 68 | "clientIp": "1.1.1.1", 69 | "cookies": {"chocolate": "chip"}, 70 | "loggedDate": 12345, 71 | "urlPattern": "test2", 72 | "headers": {"Accept": "json"}, 73 | "url": "test", 74 | "urlPath": "test3", 75 | "urlPathPattern": "test4", 76 | "browserProxyRequest": False, 77 | "loggedDateString": "1/1/2017 00:00:00+0000", 78 | "bodyPatterns": {"test": 3}, 79 | "queryParameters": {"test": 1}, 80 | "basicAuthCredentials": {"username": "username", "password": "password"}, 81 | "method": "GET", 82 | } 83 | e = NearMissMatchPatternRequest.from_dict(serialized) 84 | assert isinstance(e, NearMissMatchPatternRequest) 85 | assert e.url == "test" 86 | assert e.url_pattern == "test2" 87 | assert e.url_path == "test3" 88 | assert e.url_path_pattern == "test4" 89 | assert e.method == "GET" 90 | assert e.client_ip == "1.1.1.1" 91 | assert e.headers == {"Accept": "json"} 92 | assert e.query_parameters == {"test": 1} 93 | assert e.cookies == {"chocolate": "chip"} 94 | assertDictContainsKeyWithValue(e.body_patterns, "test", 3) 95 | assert isinstance(e.basic_auth_credentials, BasicAuthCredentials) 96 | assert e.basic_auth_credentials.username == "username" 97 | assert e.basic_auth_credentials.password == "password" 98 | assert e.browser_proxy_request == False 99 | assert e.logged_date == 12345 100 | assert e.logged_date_string == "1/1/2017 00:00:00+0000" 101 | 102 | 103 | @pytest.mark.unit 104 | @pytest.mark.serialization 105 | @pytest.mark.nearmisses 106 | def test_near_miss_match_request_serialization(): 107 | e = NearMissMatchRequest( 108 | url="test", 109 | absolute_url="test2", 110 | method=HttpMethods.GET, 111 | client_ip="1.1.1.1", 112 | headers={CommonHeaders.ACCEPT: "json"}, 113 | query_parameters={"test": 1}, 114 | cookies={"chocolate": "chip"}, 115 | basic_auth_credentials=BasicAuthCredentials( 116 | username="username", password="password" 117 | ), 118 | browser_proxy_request=False, 119 | logged_date=12345, 120 | logged_date_string="1/1/2017 00:00:00+0000", 121 | body_as_base64="test3", 122 | body="test4", 123 | ) 124 | serialized = e.get_json_data() 125 | assertDictContainsKeyWithValue(serialized, "url", "test") 126 | assertDictContainsKeyWithValue(serialized, "absoluteUrl", "test2") 127 | assertDictContainsKeyWithValue(serialized, "method", HttpMethods.GET) 128 | assertDictContainsKeyWithValue(serialized, "clientIp", "1.1.1.1") 129 | assertDictContainsKeyWithValue( 130 | serialized, "headers", {CommonHeaders.ACCEPT: "json"} 131 | ) 132 | assertDictContainsKeyWithValue(serialized, "queryParameters", {"test": 1}) 133 | assertDictContainsKeyWithValue( 134 | serialized, 135 | "cookies", 136 | { 137 | "chocolate": "chip", 138 | }, 139 | ) 140 | assertDictContainsKeyWithValue( 141 | serialized, 142 | "basicAuthCredentials", 143 | {"username": "username", "password": "password"}, 144 | ) 145 | assertDictContainsKeyWithValue(serialized, "browserProxyRequest", False) 146 | assertDictContainsKeyWithValue(serialized, "loggedDate", 12345) 147 | assertDictContainsKeyWithValue( 148 | serialized, "loggedDateString", "1/1/2017 00:00:00+0000" 149 | ) 150 | assertDictContainsKeyWithValue(serialized, "bodyAsBase64", "test3") 151 | assertDictContainsKeyWithValue(serialized, "body", "test4") 152 | 153 | 154 | @pytest.mark.unit 155 | @pytest.mark.serialization 156 | @pytest.mark.nearmisses 157 | def test_near_miss_match_request_deserialization(): 158 | serialized = { 159 | "clientIp": "1.1.1.1", 160 | "cookies": {"chocolate": "chip"}, 161 | "loggedDate": 12345, 162 | "absoluteUrl": "test2", 163 | "headers": {"Accept": "json"}, 164 | "url": "test", 165 | "browserProxyRequest": False, 166 | "body": "test4", 167 | "bodyAsBase64": "test3", 168 | "loggedDateString": "1/1/2017 00:00:00+0000", 169 | "queryParameters": {"test": 1}, 170 | "basicAuthCredentials": {"username": "username", "password": "password"}, 171 | "method": "GET", 172 | } 173 | e = NearMissMatchRequest.from_dict(serialized) 174 | assert isinstance(e, NearMissMatchRequest) 175 | assert e.url == "test" 176 | assert e.absolute_url == "test2" 177 | assert e.method == "GET" 178 | assert e.client_ip == "1.1.1.1" 179 | assertDictContainsKeyWithValue(e.headers, "Accept", "json") 180 | assertDictContainsKeyWithValue(e.query_parameters, "test", 1) 181 | assertDictContainsKeyWithValue(e.cookies, "chocolate", "chip") 182 | assert isinstance(e.basic_auth_credentials, BasicAuthCredentials) 183 | assert e.basic_auth_credentials.username == "username" 184 | assert e.basic_auth_credentials.password == "password" 185 | assert e.browser_proxy_request == False 186 | assert e.logged_date == 12345 187 | assert e.logged_date_string == "1/1/2017 00:00:00+0000" 188 | assert e.body_as_base64 == "test3" 189 | assert e.body == "test4" 190 | 191 | 192 | @pytest.mark.unit 193 | @pytest.mark.serialization 194 | @pytest.mark.nearmisses 195 | def test_near_miss_match_result_deserialization(): 196 | serialized = {"distance": 0.75} 197 | e = NearMissMatchResult.from_dict(serialized) 198 | assert isinstance(e, NearMissMatchResult) 199 | assert e.distance == 0.75 200 | 201 | 202 | @pytest.mark.unit 203 | @pytest.mark.serialization 204 | @pytest.mark.nearmisses 205 | def test_near_miss_request_pattern_result_serialization(): 206 | e = NearMissRequestPatternResult( 207 | url="test", 208 | absolute_url="test2", 209 | method=HttpMethods.GET, 210 | client_ip="1.1.1.1", 211 | headers={CommonHeaders.ACCEPT: "json"}, 212 | query_parameters={"test": 1}, 213 | cookies={"chocolate": "chip"}, 214 | basic_auth_credentials=BasicAuthCredentials( 215 | username="username", password="password" 216 | ), 217 | browser_proxy_request=False, 218 | body_as_base64="test3", 219 | body="test4", 220 | ) 221 | serialized = e.get_json_data() 222 | assertDictContainsKeyWithValue(serialized, "url", "test") 223 | assertDictContainsKeyWithValue(serialized, "absoluteUrl", "test2") 224 | assertDictContainsKeyWithValue(serialized, "method", "GET") 225 | assertDictContainsKeyWithValue(serialized, "clientIp", "1.1.1.1") 226 | assertDictContainsKeyWithValue(serialized, "headers", {"Accept": "json"}) 227 | assertDictContainsKeyWithValue(serialized, "queryParameters", {"test": 1}) 228 | assertDictContainsKeyWithValue( 229 | serialized, 230 | "cookies", 231 | { 232 | "chocolate": "chip", 233 | }, 234 | ) 235 | assertDictContainsKeyWithValue( 236 | serialized, 237 | "basicAuthCredentials", 238 | {"username": "username", "password": "password"}, 239 | ) 240 | assertDictContainsKeyWithValue(serialized, "browserProxyRequest", False) 241 | assertDictContainsKeyWithValue(serialized, "bodyAsBase64", "test3") 242 | assertDictContainsKeyWithValue(serialized, "body", "test4") 243 | 244 | 245 | @pytest.mark.unit 246 | @pytest.mark.serialization 247 | @pytest.mark.nearmisses 248 | def test_near_miss_request_pattern_result_deserialization(): 249 | serialized = { 250 | "clientIp": "1.1.1.1", 251 | "cookies": {"chocolate": "chip"}, 252 | "absoluteUrl": "test2", 253 | "headers": {"Accept": "json"}, 254 | "url": "test", 255 | "browserProxyRequest": False, 256 | "body": "test4", 257 | "bodyAsBase64": "test3", 258 | "queryParameters": {"test": 1}, 259 | "basicAuthCredentials": { 260 | "username": "username", 261 | "password": "password", 262 | }, 263 | "method": "GET", 264 | } 265 | e = NearMissRequestPatternResult.from_dict(serialized) 266 | assert isinstance(e, NearMissRequestPatternResult) 267 | assert "test" == e.url 268 | assert "test2" == e.absolute_url 269 | assert "GET" == e.method 270 | assert "1.1.1.1" == e.client_ip 271 | assert {"Accept": "json"} == e.headers 272 | assert {"test": 1} == e.query_parameters 273 | assert {"chocolate": "chip"} == e.cookies 274 | assert isinstance(e.basic_auth_credentials, BasicAuthCredentials) 275 | assert "username" == e.basic_auth_credentials.username 276 | assert "password" == e.basic_auth_credentials.password 277 | assert False is e.browser_proxy_request 278 | assert e.body_as_base64 == "test3" 279 | assert e.body == "test4" 280 | 281 | 282 | @pytest.mark.unit 283 | @pytest.mark.serialization 284 | @pytest.mark.nearmisses 285 | def test_near_miss_match_serialization(): 286 | e = NearMissMatch( 287 | request=NearMissMatchRequest(url="test"), 288 | request_pattern=NearMissRequestPatternResult(url="test2"), 289 | match_result=NearMissMatchResult(distance=0.75), 290 | ) 291 | serialized = e.get_json_data() 292 | assertDictContainsKeyWithValueType(serialized, "request", dict) 293 | request = serialized["request"] 294 | assertDictContainsKeyWithValue(request, "url", "test") 295 | 296 | assertDictContainsKeyWithValueType(serialized, "requestPattern", dict) 297 | request_pattern = serialized["requestPattern"] 298 | assertDictContainsKeyWithValue(request_pattern, "url", "test2") 299 | 300 | assertDictContainsKeyWithValueType(serialized, "matchResult", dict) 301 | match_result = serialized["matchResult"] 302 | assertDictContainsKeyWithValue(match_result, "distance", 0.75) 303 | 304 | 305 | @pytest.mark.unit 306 | @pytest.mark.serialization 307 | @pytest.mark.nearmisses 308 | def test_near_miss_match_deserialization(): 309 | serialized = { 310 | "request": { 311 | "clientIp": "1.1.1.1", 312 | "cookies": {"chocolate": "chip"}, 313 | "loggedDate": 12345, 314 | "absoluteUrl": "test2", 315 | "headers": {"Accept": "json"}, 316 | "url": "test", 317 | "browserProxyRequest": False, 318 | "body": "test4", 319 | "bodyAsBase64": "test3", 320 | "loggedDateString": "1/1/2017 00:00:00+0000", 321 | "queryParameters": {"test": 1}, 322 | "basicAuthCredentials": { 323 | "username": "username", 324 | "password": "password", 325 | }, 326 | "method": "GET", 327 | }, 328 | "requestPattern": { 329 | "clientIp": "1.1.1.1", 330 | "cookies": {"chocolate": "chip"}, 331 | "absoluteUrl": "test2", 332 | "headers": {"Accept": "json"}, 333 | "url": "test", 334 | "browserProxyRequest": False, 335 | "body": "test4", 336 | "bodyAsBase64": "test3", 337 | "queryParameters": {"test": 1}, 338 | "basicAuthCredentials": { 339 | "username": "username", 340 | "password": "password", 341 | }, 342 | "method": "GET", 343 | }, 344 | "matchResult": {"distance": 0.75}, 345 | } 346 | e = NearMissMatch.from_dict(serialized) 347 | assert isinstance(e, NearMissMatch) 348 | assert isinstance(e.request, NearMissMatchRequest) 349 | assert isinstance(e.request_pattern, NearMissRequestPatternResult) 350 | assert isinstance(e.match_result, NearMissMatchResult) 351 | 352 | # request 353 | _request = serialized["request"] 354 | assertDictContainsKeyWithValue(_request, "url", "test") 355 | assertDictContainsKeyWithValue(_request, "absoluteUrl", "test2") 356 | assertDictContainsKeyWithValue(_request, "method", "GET") 357 | assertDictContainsKeyWithValue(_request, "clientIp", "1.1.1.1") 358 | assertDictContainsKeyWithValue(_request, "headers", {"Accept": "json"}) 359 | assertDictContainsKeyWithValue(_request, "queryParameters", {"test": 1}) 360 | assertDictContainsKeyWithValue(_request, "cookies", {"chocolate": "chip"}) 361 | assertDictContainsKeyWithValue(_request, "bodyAsBase64", "test3") 362 | assertDictContainsKeyWithValue(_request, "body", "test4") 363 | assertDictContainsKeyWithValue(_request, "loggedDate", 12345) 364 | assertDictContainsKeyWithValue( 365 | _request, "loggedDateString", "1/1/2017 00:00:00+0000" 366 | ) 367 | 368 | _basicAuth = serialized["request"]["basicAuthCredentials"] 369 | assertDictContainsKeyWithValueType( 370 | serialized["request"], "basicAuthCredentials", dict 371 | ) 372 | assertDictContainsKeyWithValue(_basicAuth, "username", "username") 373 | assertDictContainsKeyWithValue(_basicAuth, "password", "password") 374 | assertDictContainsKeyWithValue(serialized["request"], "browserProxyRequest", False) 375 | 376 | # request pattern 377 | _requestPattern = serialized["requestPattern"] 378 | assertDictContainsKeyWithValue(_requestPattern, "url", "test") 379 | assertDictContainsKeyWithValue(_requestPattern, "absoluteUrl", "test2") 380 | assertDictContainsKeyWithValue(_requestPattern, "method", "GET") 381 | assertDictContainsKeyWithValue(_requestPattern, "clientIp", "1.1.1.1") 382 | assertDictContainsKeyWithValue(_requestPattern, "headers", {"Accept": "json"}) 383 | assertDictContainsKeyWithValue(_requestPattern, "queryParameters", {"test": 1}) 384 | assertDictContainsKeyWithValue(_requestPattern, "cookies", {"chocolate": "chip"}) 385 | assertDictContainsKeyWithValue(_requestPattern, "bodyAsBase64", "test3") 386 | assertDictContainsKeyWithValue(_requestPattern, "body", "test4") 387 | 388 | _basicAuth = serialized["requestPattern"]["basicAuthCredentials"] 389 | assertDictContainsKeyWithValueType( 390 | serialized["requestPattern"], "basicAuthCredentials", dict 391 | ) 392 | assertDictContainsKeyWithValue(_basicAuth, "username", "username") 393 | assertDictContainsKeyWithValue(_basicAuth, "password", "password") 394 | assertDictContainsKeyWithValue( 395 | serialized["requestPattern"], "browserProxyRequest", False 396 | ) 397 | # match result 398 | assertDictContainsKeyWithValue(serialized["matchResult"], "distance", 0.75) 399 | 400 | 401 | def test_near_miss_match_response_serialization(): 402 | e = NearMissMatchResponse( 403 | near_misses=[ 404 | NearMissMatch( 405 | request=NearMissMatchRequest(url="test"), 406 | request_pattern=NearMissRequestPatternResult(url="test2"), 407 | match_result=NearMissMatchResult(distance=0.75), 408 | ) 409 | ] 410 | ) 411 | serialized = e.get_json_data() 412 | assertDictContainsKeyWithValueType(serialized, "nearMisses", list) 413 | near_miss = serialized["nearMisses"][0] 414 | assertDictContainsKeyWithValueType(near_miss, "request", dict) 415 | assertDictContainsKeyWithValue(near_miss["request"], "url", "test") 416 | assertDictContainsKeyWithValueType(near_miss, "requestPattern", dict) 417 | assertDictContainsKeyWithValue(near_miss["requestPattern"], "url", "test2") 418 | assertDictContainsKeyWithValueType(near_miss, "matchResult", dict) 419 | assertDictContainsKeyWithValue(near_miss["matchResult"], "distance", 0.75) 420 | 421 | 422 | def test_near_miss_match_response_deserialization(): 423 | serialized = { 424 | "nearMisses": [ 425 | { 426 | "request": {"url": "test"}, 427 | "requestPattern": {"url": "test"}, 428 | "matchResult": {"distance": 0.75}, 429 | } 430 | ] 431 | } 432 | e = NearMissMatchResponse.from_dict(serialized) 433 | assert isinstance(e, NearMissMatchResponse) 434 | assert isinstance(e.near_misses, list) 435 | assert len(e.near_misses) == 1 436 | near_miss = e.near_misses[0] 437 | assert isinstance(near_miss, NearMissMatch) 438 | assert isinstance(near_miss.request, NearMissMatchRequest) 439 | assert isinstance(near_miss.request_pattern, NearMissRequestPatternResult) 440 | assert isinstance(near_miss.match_result, NearMissMatchResult) 441 | -------------------------------------------------------------------------------- /tests/test_resources/test_requests/test_requests_resource.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from tests.utils import assertEqual, assertIsInstance 4 | from wiremock.client import ( 5 | NearMissMatch, 6 | NearMissMatchPatternRequest, 7 | NearMissMatchResponse, 8 | RequestCountResponse, 9 | RequestResponse, 10 | RequestResponseAll, 11 | RequestResponseAllMeta, 12 | RequestResponseDefinition, 13 | RequestResponseFindResponse, 14 | RequestResponseRequest, 15 | Requests, 16 | ) 17 | from wiremock.resources.near_misses import NearMissMatchRequest 18 | 19 | 20 | @responses.activate 21 | def test_get_all_received_requests(): 22 | e = RequestResponseAll( 23 | requests=[], 24 | meta=RequestResponseAllMeta(total=1), 25 | request_journal_disabled=False, 26 | ) 27 | resp = e.get_json_data() 28 | responses.add( 29 | responses.GET, "http://localhost/__admin/requests", json=resp, status=200 30 | ) 31 | 32 | r = Requests.get_all_received_requests() 33 | assertIsInstance(r, RequestResponseAll) 34 | assertEqual(False, r.request_journal_disabled) 35 | 36 | 37 | @responses.activate 38 | def test_get_request(): 39 | e = RequestResponse( 40 | id="1234-5678", 41 | request=RequestResponseRequest(url="test", method="GET"), 42 | response_definition=RequestResponseDefinition(url="test", method="GET"), 43 | ) 44 | resp = e.get_json_data() 45 | responses.add( 46 | responses.GET, 47 | "http://localhost/__admin/requests/1234-5678", 48 | json=resp, 49 | status=200, 50 | ) 51 | 52 | r = Requests.get_request("1234-5678") 53 | assertIsInstance(r, RequestResponse) 54 | assertEqual("test", r.request.url) 55 | assertEqual("1234-5678", r.id) 56 | 57 | 58 | @responses.activate 59 | def test_reset_request_journal(): 60 | responses.add( 61 | responses.DELETE, "http://localhost/__admin/requests", body="", status=200 62 | ) 63 | 64 | r = Requests.reset_request_journal() 65 | assertEqual(200, r.status_code) 66 | 67 | 68 | @responses.activate 69 | def test_get_matching_request_count(): 70 | resp = RequestCountResponse(count=4).get_json_data() 71 | responses.add( 72 | responses.POST, "http://localhost/__admin/requests/count", json=resp, status=200 73 | ) 74 | 75 | request = NearMissMatchPatternRequest(url="test", method="GET") 76 | 77 | r = Requests.get_matching_request_count(request) 78 | assertIsInstance(r, RequestCountResponse) 79 | assertEqual(4, r.count) 80 | 81 | 82 | @responses.activate 83 | def test_get_matching_requests(): 84 | e = RequestResponseFindResponse( 85 | requests=[ 86 | RequestResponseRequest(method="GET", url="test"), 87 | ], 88 | ) 89 | resp = e.get_json_data() 90 | responses.add( 91 | responses.POST, "http://localhost/__admin/requests/find", json=resp, status=200 92 | ) 93 | 94 | request = NearMissMatchPatternRequest(url="test", method="GET") 95 | 96 | r = Requests.get_matching_requests(request) 97 | assertIsInstance(r, RequestResponseFindResponse) 98 | assertIsInstance(r.requests, list) 99 | assertEqual(1, len(r.requests)) 100 | result = r.requests[0] 101 | assertIsInstance(result, RequestResponseRequest) 102 | assertEqual("GET", result.method) 103 | assertEqual("test", result.url) 104 | 105 | 106 | @responses.activate 107 | def test_get_unmatched_requests(): 108 | e = RequestResponseFindResponse( 109 | requests=[ 110 | RequestResponseRequest(method="GET", url="test"), 111 | ], 112 | ) 113 | resp = e.get_json_data() 114 | responses.add( 115 | responses.GET, 116 | "http://localhost/__admin/requests/unmatched", 117 | json=resp, 118 | status=200, 119 | ) 120 | 121 | r = Requests.get_unmatched_requests() 122 | assertIsInstance(r, RequestResponseFindResponse) 123 | assertIsInstance(r.requests, list) 124 | assertEqual(1, len(r.requests)) 125 | result = r.requests[0] 126 | assertIsInstance(result, RequestResponseRequest) 127 | assertEqual("GET", result.method) 128 | assertEqual("test", result.url) 129 | 130 | 131 | @responses.activate 132 | def test_get_unmatched_requests_near_misses(): 133 | e = NearMissMatchResponse( 134 | near_misses=[ 135 | NearMissMatch(request=NearMissMatchRequest(url="test", method="GET")), 136 | ] 137 | ) 138 | resp = e.get_json_data() 139 | responses.add( 140 | responses.GET, 141 | "http://localhost/__admin/requests/unmatched/near-misses", 142 | json=resp, 143 | status=200, 144 | ) 145 | 146 | r = Requests.get_unmatched_requests_near_misses() 147 | assertIsInstance(r, NearMissMatchResponse) 148 | result = r.near_misses[0] 149 | assertIsInstance(result, NearMissMatch) 150 | assertEqual("test", result.request.url) 151 | assertEqual("GET", result.request.method) 152 | -------------------------------------------------------------------------------- /tests/test_resources/test_scenarios/test_scenario_resource.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from tests.utils import assertEqual 4 | from wiremock.client import Scenarios 5 | 6 | 7 | @responses.activate 8 | def test_reset_scenarios(): 9 | responses.add( 10 | responses.POST, 11 | "http://localhost/__admin/scenarios/reset", 12 | body="", 13 | status=200, 14 | ) 15 | 16 | r = Scenarios.reset_all_scenarios() 17 | assertEqual(200, r.status_code) 18 | -------------------------------------------------------------------------------- /tests/test_resources/test_scenarios/test_scenario_serialization.py: -------------------------------------------------------------------------------- 1 | # Purposefully left blank as there are no specific models. 2 | -------------------------------------------------------------------------------- /tests/test_resources/test_settings/test_settings_resource.py: -------------------------------------------------------------------------------- 1 | import responses 2 | 3 | from tests.utils import assertEqual, assertIsInstance 4 | from wiremock.client import GlobalSetting, GlobalSettings 5 | 6 | 7 | @responses.activate 8 | def test_update_settings(): 9 | e = GlobalSetting(fixed_delay=500) 10 | resp = e.get_json_data() 11 | responses.add( 12 | responses.POST, "http://localhost/__admin/settings", json=resp, status=200 13 | ) 14 | 15 | r = GlobalSettings.update_global_settings(e) 16 | assertIsInstance(r, GlobalSetting) 17 | assertEqual(500, r.fixed_delay) 18 | -------------------------------------------------------------------------------- /tests/test_resources/test_settings/test_settings_serialization.py: -------------------------------------------------------------------------------- 1 | from tests.utils import assertDictContainsKeyWithValue, assertEqual, assertIsInstance 2 | from wiremock.resources.settings import GlobalSetting 3 | 4 | 5 | def test_global_settings_serialization(): 6 | gs = GlobalSetting(fixed_delay=500) 7 | serialized = gs.get_json_data() 8 | assertDictContainsKeyWithValue(serialized, "fixedDelay", 500) 9 | 10 | 11 | def test_global_settings_deserialization(): 12 | serialized = {"fixedDelay": 500} 13 | gs = GlobalSetting.from_dict(serialized) 14 | assertIsInstance(gs, GlobalSetting) 15 | assertEqual(500, gs.fixed_delay) 16 | -------------------------------------------------------------------------------- /tests/test_responses/test_requests/test_requests_serialization.py: -------------------------------------------------------------------------------- 1 | from tests.utils import ( 2 | assertDictContainsKeyWithValue, 3 | assertDictContainsKeyWithValueType, 4 | assertEqual, 5 | assertIsInstance, 6 | ) 7 | from wiremock.resources.mappings import BasicAuthCredentials 8 | from wiremock.resources.requests import ( 9 | RequestCountResponse, 10 | RequestResponse, 11 | RequestResponseAll, 12 | RequestResponseAllMeta, 13 | RequestResponseDefinition, 14 | RequestResponseFindResponse, 15 | RequestResponseRequest, 16 | ) 17 | 18 | 19 | def test_request_count_response_serialization(): 20 | e = RequestCountResponse(count=1) 21 | serialized = e.get_json_data() 22 | assertDictContainsKeyWithValue(serialized, "count", 1) 23 | 24 | 25 | def test_request_count_response_deserialization(): 26 | serialized = {"count": 1} 27 | e = RequestCountResponse.from_dict(serialized) 28 | assertIsInstance(e, RequestCountResponse) 29 | assertEqual(1, e.count) 30 | 31 | 32 | def test_request_response_all_meta_serialization(): 33 | e = RequestResponseAllMeta(total=1) 34 | serialized = e.get_json_data() 35 | assertDictContainsKeyWithValue(serialized, "total", 1) 36 | 37 | 38 | def test_request_response_all_meta_deserialization(): 39 | serialized = {"total": 1} 40 | e = RequestResponseAllMeta.from_dict(serialized) 41 | assertIsInstance(e, RequestResponseAllMeta) 42 | assertEqual(1, e.total) 43 | 44 | 45 | def test_request_response_request_serialization(): 46 | e = RequestResponseRequest( 47 | method="GET", 48 | url="test", 49 | absolute_url="test2", 50 | client_ip="test3", 51 | basic_auth_credentials=BasicAuthCredentials( 52 | username="username", password="password" 53 | ), 54 | cookies={"chocolate": "chip"}, 55 | headers={"test": "1"}, 56 | query_parameters={"test2": "2"}, 57 | browser_proxy_request=False, 58 | body="test4", 59 | body_as_base64="test5", 60 | logged_date=12345, 61 | logged_date_string="test6", 62 | ) 63 | serialized = e.get_json_data() 64 | assertDictContainsKeyWithValue(serialized, "method", "GET") 65 | assertDictContainsKeyWithValue(serialized, "url", "test") 66 | assertDictContainsKeyWithValue(serialized, "absoluteUrl", "test2") 67 | assertDictContainsKeyWithValue(serialized, "clientIp", "test3") 68 | assertDictContainsKeyWithValue( 69 | serialized, 70 | "basicAuthCredentials", 71 | {"username": "username", "password": "password"}, 72 | ) 73 | assertDictContainsKeyWithValue(serialized, "cookies", {"chocolate": "chip"}) 74 | assertDictContainsKeyWithValue(serialized, "headers", {"test": "1"}) 75 | assertDictContainsKeyWithValue(serialized, "queryParameters", {"test2": "2"}) 76 | assertDictContainsKeyWithValue(serialized, "browserProxyRequest", False) 77 | assertDictContainsKeyWithValue(serialized, "body", "test4") 78 | assertDictContainsKeyWithValue(serialized, "bodyAsBase64", "test5") 79 | assertDictContainsKeyWithValue(serialized, "loggedDate", 12345) 80 | assertDictContainsKeyWithValue(serialized, "loggedDateString", "test6") 81 | 82 | 83 | def test_request_response_request_deserialization(): 84 | serialized = { 85 | "method": "GET", 86 | "url": "test", 87 | "absoluteUrl": "test2", 88 | "clientIp": "test3", 89 | "basicAuthCredentials": {"username": "username", "password": "password"}, 90 | "cookies": {"chocolate": "chip"}, 91 | "headers": {"test": "1"}, 92 | "queryParameters": {"test2": "2"}, 93 | "browserProxyRequest": False, 94 | "body": "test4", 95 | "bodyAsBase64": "test5", 96 | "loggedDate": 12345, 97 | "loggedDateString": "test6", 98 | } 99 | e = RequestResponseRequest.from_dict(serialized) 100 | assertIsInstance(e, RequestResponseRequest) 101 | assertEqual("GET", e.method) 102 | assertEqual("test", e.url) 103 | assertEqual("test2", e.absolute_url) 104 | assertEqual("test3", e.client_ip) 105 | assertIsInstance(e.basic_auth_credentials, BasicAuthCredentials) 106 | assertEqual("username", e.basic_auth_credentials.username) 107 | assertEqual("password", e.basic_auth_credentials.password) 108 | assertEqual({"chocolate": "chip"}, e.cookies) 109 | assertEqual({"test": "1"}, e.headers) 110 | assertEqual({"test2": "2"}, e.query_parameters) 111 | assertEqual(False, e.browser_proxy_request) 112 | assertEqual("test4", e.body) 113 | assertEqual("test5", e.body_as_base64) 114 | assertEqual(12345, e.logged_date) 115 | assertEqual("test6", e.logged_date_string) 116 | 117 | 118 | def test_request_response_definition_serialization(): 119 | e = RequestResponseDefinition( 120 | status=200, 121 | transformers=["test"], 122 | from_configured_stub=False, 123 | transformer_parameters={"test2": "2"}, 124 | ) 125 | serialized = e.get_json_data() 126 | assertDictContainsKeyWithValue(serialized, "status", 200) 127 | assertDictContainsKeyWithValue(serialized, "transformers", ["test"]) 128 | assertDictContainsKeyWithValue(serialized, "fromConfiguredStub", False) 129 | assertDictContainsKeyWithValue(serialized, "transformerParameters", {"test2": "2"}) 130 | 131 | 132 | def test_request_response_definition_deserialization(): 133 | serialized = { 134 | "status": 200, 135 | "transformers": ["test"], 136 | "fromConfiguredStub": False, 137 | "transformerParameters": {"test2": "2"}, 138 | } 139 | e = RequestResponseDefinition.from_dict(serialized) 140 | assertIsInstance(e, RequestResponseDefinition) 141 | assertEqual(200, e.status) 142 | assertEqual(["test"], e.transformers) 143 | assertEqual(False, e.from_configured_stub) 144 | assertEqual({"test2": "2"}, e.transformer_parameters) 145 | 146 | 147 | def test_request_response_serialization(): 148 | e = RequestResponse( 149 | request=RequestResponseRequest(method="GET", url="test"), 150 | response_definition=RequestResponseDefinition(status=200), 151 | ) 152 | serialized = e.get_json_data() 153 | assertDictContainsKeyWithValue( 154 | serialized, "request", {"method": "GET", "url": "test"} 155 | ) 156 | assertDictContainsKeyWithValue(serialized, "responseDefinition", {"status": 200}) 157 | 158 | 159 | def test_request_response_deserialization(): 160 | serialized = { 161 | "request": {"method": "GET", "url": "test"}, 162 | "responseDefinition": {"status": 200}, 163 | } 164 | e = RequestResponse.from_dict(serialized) 165 | assertIsInstance(e, RequestResponse) 166 | assertIsInstance(e.request, RequestResponseRequest) 167 | assertEqual("GET", e.request.method) 168 | assertEqual("test", e.request.url) 169 | assertIsInstance(e.response_definition, RequestResponseDefinition) 170 | assertEqual(200, e.response_definition.status) 171 | 172 | 173 | def test_request_response_find_response_serialization(): 174 | e = RequestResponseFindResponse( 175 | requests=[ 176 | RequestResponseRequest(method="GET", url="test"), 177 | ] 178 | ) 179 | serialized = e.get_json_data() 180 | assertDictContainsKeyWithValueType(serialized, "requests", list) 181 | assertDictContainsKeyWithValue( 182 | serialized, 183 | "requests", 184 | [ 185 | {"method": "GET", "url": "test"}, 186 | ], 187 | ) 188 | 189 | 190 | def test_request_response_find_response_deserialization(): 191 | serialized = { 192 | "requests": [ 193 | {"method": "GET", "url": "test"}, 194 | ] 195 | } 196 | e = RequestResponseFindResponse.from_dict(serialized) 197 | assertIsInstance(e, RequestResponseFindResponse) 198 | assertIsInstance(e.requests, list) 199 | assertIsInstance(e.requests[0], RequestResponseRequest) 200 | assertEqual("GET", e.requests[0].method) 201 | assertEqual("test", e.requests[0].url) 202 | 203 | 204 | def test_request_response_all_serialization(): 205 | e = RequestResponseAll( 206 | requests=[ 207 | RequestResponse( 208 | request=RequestResponseRequest(method="GET", url="test"), 209 | response_definition=RequestResponseDefinition(status=200), 210 | ), 211 | ], 212 | meta=RequestResponseAllMeta(total=1), 213 | request_journal_disabled=False, 214 | ) 215 | serialized = e.get_json_data() 216 | assertDictContainsKeyWithValue(serialized, "requestJournalDisabled", False) 217 | assertDictContainsKeyWithValue( 218 | serialized, 219 | "requests", 220 | [ 221 | { 222 | "request": {"method": "GET", "url": "test"}, 223 | "responseDefinition": {"status": 200}, 224 | }, 225 | ], 226 | ) 227 | assertDictContainsKeyWithValue(serialized, "meta", {"total": 1}) 228 | 229 | 230 | def test_request_response_all_deserialization(): 231 | serialized = { 232 | "requests": [ 233 | { 234 | "request": {"method": "GET", "url": "test"}, 235 | "responseDefinition": {"status": 200}, 236 | }, 237 | ], 238 | "meta": {"total": 1}, 239 | "requestJournalDisabled": False, 240 | } 241 | e = RequestResponseAll.from_dict(serialized) 242 | assertIsInstance(e, RequestResponseAll) 243 | assertEqual(False, e.request_journal_disabled) 244 | assertIsInstance(e.requests, list) 245 | rr = e.requests[0] 246 | assertIsInstance(rr, RequestResponse) 247 | assertIsInstance(rr.request, RequestResponseRequest) 248 | assertEqual("GET", rr.request.method) 249 | assertEqual("test", rr.request.url) 250 | assertIsInstance(rr.response_definition, RequestResponseDefinition) 251 | assertEqual(200, rr.response_definition.status) 252 | assertIsInstance(e.meta, RequestResponseAllMeta) 253 | assertEqual(1, e.meta.total) 254 | -------------------------------------------------------------------------------- /tests/test_server/test_server.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from subprocess import PIPE, STDOUT 3 | from unittest.mock import DEFAULT, patch 4 | 5 | import pytest 6 | import responses 7 | from importlib_resources import files 8 | 9 | from tests.utils import assertEqual, assertIsInstance 10 | from wiremock.server.exceptions import ( 11 | WireMockServerAlreadyStartedError, 12 | WireMockServerNotStartedError, 13 | ) 14 | from wiremock.server.server import WireMockServer 15 | 16 | 17 | @pytest.fixture() 18 | def config(): 19 | @dataclass 20 | class ServerConfig: 21 | 22 | java_path: str 23 | jar_path: str 24 | port: int 25 | 26 | return ServerConfig( 27 | java_path="/path/to/java", 28 | jar_path="/path/to/jar", 29 | port=54321, 30 | ) 31 | 32 | 33 | @pytest.fixture(scope="function") 34 | def server(config): 35 | with patch.object(WireMockServer, "_get_free_port", return_value=config.port): 36 | yield WireMockServer(java_path=config.java_path, jar_path=config.jar_path) 37 | 38 | 39 | @pytest.mark.usefixtures("server") 40 | def test_init(config): 41 | with patch.object(WireMockServer, "_get_free_port") as _get_free_port: 42 | _get_free_port.return_value = config.port 43 | 44 | wm = WireMockServer(java_path=config.java_path, jar_path=config.jar_path) 45 | 46 | assertEqual(wm.port, _get_free_port.return_value) 47 | 48 | assertEqual(wm.java_path, config.java_path) 49 | assertEqual(wm.jar_path, config.jar_path) 50 | assert not wm.is_running 51 | 52 | 53 | def test_init_with_defaults(config): 54 | with patch.object(WireMockServer, "_get_free_port", return_value=config.port): 55 | wm = WireMockServer() 56 | 57 | expected_jar = files("wiremock") / "server" / "wiremock-standalone-2.35.1.jar" 58 | assertEqual(wm.java_path, "java") # Assume java in PATH 59 | assertEqual(wm.jar_path, expected_jar) 60 | 61 | 62 | @patch("wiremock.server.server.socket") 63 | def test_get_free_port(mock_socket, server): 64 | sock = mock_socket.socket.return_value 65 | expected_port = 54321 66 | sock.getsockname.return_value = ("localhost", expected_port) 67 | 68 | port = server._get_free_port() 69 | 70 | assertEqual(port, expected_port) 71 | 72 | 73 | @patch("wiremock.server.server.atexit") 74 | @patch("wiremock.server.server.Popen") 75 | @responses.activate 76 | def test_start(Popen, atexit, config, server): 77 | # mock healthy endpoint 78 | responses.add( 79 | responses.GET, 80 | "http://localhost:{}/__admin".format(server.port), 81 | json=[], 82 | status=200, 83 | ) 84 | 85 | def poll(): 86 | Popen.return_value.returncode = None 87 | return None 88 | 89 | Popen.return_value.poll.side_effect = poll 90 | 91 | server.start() 92 | 93 | Popen.assert_called_once_with( 94 | [ 95 | config.java_path, 96 | "-jar", 97 | config.jar_path, 98 | "--port", 99 | str(54321), 100 | "--local-response-templating", 101 | ], 102 | stdin=PIPE, 103 | stdout=PIPE, 104 | stderr=STDOUT, 105 | ) 106 | 107 | assert server.is_running is True 108 | atexit.register.assert_called_once_with(server.stop, raise_on_error=False) 109 | 110 | # Test when already started 111 | with pytest.raises(WireMockServerAlreadyStartedError): 112 | server.start() 113 | 114 | 115 | def test_start_with_invalid_java(): 116 | wm = WireMockServer(java_path="/no/such/path") 117 | with pytest.raises(WireMockServerNotStartedError): 118 | wm.start() 119 | 120 | 121 | def test_start_with_invalid_jar(): 122 | wm = WireMockServer(jar_path="/dev/null") 123 | with pytest.raises(WireMockServerNotStartedError): 124 | wm.start() 125 | 126 | 127 | def test_stop(server): 128 | with patch.object(server, "_WireMockServer__subprocess") as _subprocess: 129 | server._WireMockServer__running = True 130 | 131 | server.stop() 132 | 133 | _subprocess.kill.assert_called_once_with() 134 | 135 | # Test repeated call 136 | 137 | _subprocess.kill.side_effect = AttributeError 138 | with pytest.raises(WireMockServerNotStartedError): 139 | server.stop() 140 | 141 | 142 | def test_with_statement(): 143 | with patch.multiple(WireMockServer, start=DEFAULT, stop=DEFAULT) as mocks: 144 | 145 | with WireMockServer() as wm: 146 | assertIsInstance(wm, WireMockServer) 147 | mocks["start"].assert_called_once_with() 148 | 149 | mocks["stop"].assert_called_once_with() 150 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | def assertDictContainsKeyWithValue(obj, key, value): 2 | assert key in obj, f"{key} is not in dict: {obj}" 3 | assert obj[key] == value, f"{obj[key]} does not equal {value}" 4 | 5 | 6 | def assertDictContainsKeyWithValueType(obj, key, value): 7 | assert key in obj, f"{key} is not in dict: {obj}" 8 | assert isinstance(obj[key], value) 9 | 10 | 11 | def assertIsInstance(obja, objb): 12 | 13 | assert isinstance(obja, objb) 14 | 15 | 16 | def assertEqual(obja, objb): 17 | 18 | assert obja == objb 19 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37, py38, py39, py310, py311 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-cov 8 | 9 | commands = 10 | pytest --cov=my_library tests/ 11 | -------------------------------------------------------------------------------- /wiremock/VERSION: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /wiremock/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | __wiremock_version_path__ = os.path.realpath(__file__ + '/../VERSION') 4 | with open(__wiremock_version_path__, 'r') as f: 5 | __version__ = f.readline().strip() 6 | -------------------------------------------------------------------------------- /wiremock/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import types 3 | 4 | 5 | def with_metaclass(meta, *bases): 6 | """Create a base class with a metaclass.""" 7 | # This requires a bit of explanation: the basic idea is to make a dummy 8 | # metaclass for one level of class instantiation that replaces itself with 9 | # the actual metaclass. 10 | class metaclass(type): 11 | def __new__(cls, name, this_bases, d): 12 | if sys.version_info[:2] >= (3, 7): 13 | # This version introduced PEP 560 that requires a bit 14 | # of extra care (we mimic what is done by __build_class__). 15 | resolved_bases = types.resolve_bases(bases) 16 | if resolved_bases is not bases: 17 | d["__orig_bases__"] = bases 18 | else: 19 | resolved_bases = bases 20 | return meta(name, resolved_bases, d) 21 | 22 | @classmethod 23 | def __prepare__(cls, name, this_bases): 24 | return meta.__prepare__(name, bases) 25 | 26 | return type.__new__(metaclass, "temporary_class", (), {}) 27 | 28 | 29 | def add_metaclass(metaclass): 30 | """Class decorator for creating a class with a metaclass.""" 31 | 32 | def wrapper(cls): 33 | orig_vars = cls.__dict__.copy() 34 | slots = orig_vars.get("__slots__") 35 | if slots is not None: 36 | if isinstance(slots, str): 37 | slots = [slots] 38 | for slots_var in slots: 39 | orig_vars.pop(slots_var) 40 | orig_vars.pop("__dict__", None) 41 | orig_vars.pop("__weakref__", None) 42 | if hasattr(cls, "__qualname__"): 43 | orig_vars["__qualname__"] = cls.__qualname__ 44 | return metaclass(cls.__name__, cls.__bases__, orig_vars) 45 | 46 | return wrapper 47 | -------------------------------------------------------------------------------- /wiremock/base/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_entity import * 2 | from .base_resource import BaseResource, RestClient 3 | -------------------------------------------------------------------------------- /wiremock/base/base_entity.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import json 3 | 4 | from wiremock._compat import add_metaclass 5 | 6 | 7 | class JsonPropertyValueContainer(object): 8 | def __init__(self, json_property, value): 9 | self.json_property = json_property 10 | self.value = value 11 | 12 | def getval(self): 13 | return self.value 14 | 15 | def setval(self, value): 16 | self.value = value 17 | 18 | def delval(self): # pragma: no cover 19 | self.value = None 20 | 21 | def get_property(self): # pragma: no cover 22 | _get = lambda slf: self.getval() 23 | _set = lambda slf, val: self.setval(val) 24 | _del = lambda slf: self.delval() 25 | 26 | return property(_get, _set, _del) 27 | 28 | 29 | class JsonProperty(object): 30 | 31 | value_container = JsonPropertyValueContainer 32 | 33 | def __init__(self, json_name, property_name=None, klass=None, list_klass=None, dict_key_klass=None, dict_value_klass=None, include_if_null=False): 34 | self._json_name = json_name 35 | self._property_name = property_name 36 | self._klass = klass 37 | 38 | if self._klass is not None and not issubclass(self._klass, BaseAbstractEntity): 39 | if issubclass(self._klass, list) or issubclass(self._klass, tuple): 40 | self._list_klass = list_klass 41 | else: 42 | self._list_klass = None 43 | 44 | if issubclass(self._klass, dict): 45 | self._dict_key_klass = dict_key_klass 46 | self._dict_value_klass = dict_value_klass 47 | else: 48 | self._dict_key_klass = None 49 | self._dict_value_klass = None 50 | else: 51 | self._list_klass = None 52 | self._dict_key_klass = None 53 | self._dict_value_klass = None 54 | 55 | self._include_if_null = include_if_null 56 | 57 | def set_property_name(self, property_name): 58 | self._property_name = property_name 59 | if self._json_name is None: # pragma: no cover 60 | self._json_name = self._property_name 61 | 62 | @property 63 | def json_name(self): 64 | return self._json_name 65 | 66 | @property 67 | def property_name(self): 68 | return self._property_name 69 | 70 | @property 71 | def klass(self): 72 | return self._klass 73 | 74 | @property 75 | def list_klass(self): 76 | return self._list_klass 77 | 78 | @property 79 | def dict_key_klass(self): 80 | return self._dict_key_klass 81 | 82 | @property 83 | def dict_value_klass(self): 84 | return self._dict_value_klass 85 | 86 | def is_dict(self): 87 | return self._klass is not None and issubclass(self._klass, dict) 88 | 89 | def is_list(self): 90 | return self._klass is not None and (issubclass(self._klass, list) or issubclass(self._klass, tuple)) 91 | 92 | def is_base_entity_class(self): 93 | return self._klass is not None and issubclass(self._klass, BaseAbstractEntity) 94 | 95 | @property 96 | def include_if_null(self): 97 | return self._include_if_null 98 | 99 | def __str__(self): 100 | return "JsonProperty(json_name={}, property_name={}, klass={}, include_if_null={})".format( 101 | self._json_name, self._property_name, self._klass, self._include_if_null 102 | ) 103 | 104 | def __unicode__(self): 105 | return "JsonProperty(json_name={}, property_name={}, klass={}, include_if_null={})".format( 106 | self._json_name, self._property_name, self._klass, self._include_if_null 107 | ) 108 | 109 | 110 | class EntityModelException(Exception): 111 | pass 112 | 113 | 114 | class BaseAbstractEntity(object): 115 | """ 116 | The base abstract entity class, don't inherit from this, inherit from BaseEntity, defined below. 117 | """ 118 | 119 | def __init__(self, **values): 120 | self._values = {} 121 | for name, prop in self._properties.items(): 122 | value = values.get(prop.json_name, values.get(name, None)) 123 | if prop.is_list() and isinstance(value, (tuple, list)): # This is a list with sub types 124 | l = prop.klass() 125 | for v in value: 126 | if prop.list_klass is not None and issubclass(prop.list_klass, BaseAbstractEntity): 127 | l.append(prop.list_klass.from_dict(v)) 128 | else: 129 | l.append(v) 130 | value = l 131 | value_container = prop.value_container(prop, l) 132 | elif prop.is_dict() and isinstance(value, dict): # This is a dict with sub types 133 | d = {} 134 | for k, v in value.items(): 135 | rk = k 136 | rv = v 137 | 138 | if prop.dict_key_klass is None: 139 | pass 140 | elif issubclass(prop.dict_key_klass, BaseAbstractEntity): 141 | rk = prop.dict_key_klass.from_dict(k) 142 | else: 143 | rk = prop.dict_key_klass(k) 144 | 145 | if prop.dict_value_klass is None: 146 | pass 147 | elif issubclass(prop.dict_value_klass, BaseAbstractEntity): 148 | rv = prop.dict_value_klass.from_dict(v) 149 | else: 150 | rv = prop.dict_value_klass(v) 151 | 152 | d[rk] = rv 153 | value = d 154 | value_container = prop.value_container(prop, d) 155 | elif prop.is_base_entity_class() and isinstance(value, dict): # This is a BaseAbstractEntity Class 156 | value = prop.klass.from_dict(value) 157 | value_container = prop.value_container(prop, value) 158 | else: 159 | value_container = prop.value_container(prop, value) 160 | self._values[name] = value_container 161 | setattr(self, name, value) 162 | 163 | def __eq__(self, other): 164 | if not isinstance(other, BaseAbstractEntity): # pragma: no cover 165 | return False 166 | 167 | return self.get_json_data() == other.get_json_data() 168 | 169 | def __ne__(self, other): # pragma: no cover 170 | return not self.__eq__(other) 171 | 172 | @classmethod 173 | def from_json(cls, json_string): 174 | return cls(**json.loads(json_string)) 175 | 176 | @classmethod 177 | def from_dict(cls, json_dict): 178 | return cls(**json_dict) 179 | 180 | def get_json_data(self): 181 | """ 182 | Get the Dict of the properties 183 | 184 | :return: dict of properties 185 | """ 186 | result = {} 187 | for name, prop in self._properties.items(): 188 | item = getattr(self, name, None) 189 | if item is not None: 190 | if isinstance(item, BaseAbstractEntity): 191 | item = item.get_json_data() 192 | elif isinstance(item, (tuple, list)): 193 | tmp = [] 194 | for i in item: 195 | if isinstance(i, BaseAbstractEntity): 196 | tmp.append(i.get_json_data()) 197 | else: 198 | tmp.append(i) 199 | item = tmp 200 | elif isinstance(item, dict): 201 | tmp = {} 202 | for k, v in item.items(): 203 | if isinstance(v, BaseAbstractEntity): 204 | tmp[k] = v.get_json_data() 205 | else: 206 | tmp[k] = v 207 | item = tmp 208 | if item is not None or prop.include_if_null: 209 | result[prop.json_name] = item 210 | return result 211 | 212 | def to_json(self): 213 | """ 214 | 215 | :return: json string of the data 216 | """ 217 | return json.dumps(self.get_json_data()) 218 | 219 | def __getitem__(self, item): 220 | value = self._properties.get(item, None) 221 | if value is not None: 222 | return getattr(self, item) 223 | raise AttributeError(item) 224 | 225 | def __setitem__(self, key, value): 226 | prop = self._properties.get(key) 227 | if prop is not None: 228 | setattr(key, value) 229 | 230 | def __delitem__(self, key): 231 | prop = self._properties.get(key) 232 | if prop is not None: 233 | delattr(self, key) 234 | else: 235 | raise AttributeError(key) 236 | 237 | def __contains__(self, item): 238 | return item in self._properties.keys() 239 | 240 | def __len__(self): 241 | return len(self._properties) 242 | 243 | def __iter__(self): 244 | for item in self._properties.keys(): 245 | yield item 246 | 247 | def items(self): 248 | items = [] 249 | for key in self._properties.keys(): 250 | items.append((key, getattr(self, key))) 251 | return items 252 | 253 | def keys(self): 254 | return self._properties.keys() 255 | 256 | def values(self): 257 | items = [] 258 | for key in self._properties.keys(): 259 | items.append(getattr(self, key)) 260 | return items 261 | 262 | 263 | class BaseEntityMetaType(type): 264 | def __new__(mcs, name, bases, body): 265 | prop_dict = OrderedDict() 266 | 267 | for base in bases: 268 | for k, v in getattr(base, "_properties", {}).items(): 269 | prop_dict.setdefault(k, v) 270 | 271 | def _transform_property(prop_name, prop_obj): 272 | prop_dict[prop_name] = prop_obj 273 | prop_obj.set_property_name(prop_name) 274 | _get = lambda self: self._values[prop_name].getval() 275 | _set = lambda self, val: self._values[prop_name].setval(val) 276 | _del = lambda self: self._values[prop_name].delval() 277 | body[prop_name] = property(_get, _set, _del) 278 | 279 | property_definitions = [(k, v) for k, v in body.items() if isinstance(v, JsonProperty)] 280 | 281 | for k, v in property_definitions: 282 | _transform_property(k, v) 283 | 284 | json_names = set() 285 | for v in prop_dict.values(): 286 | # type: v -> EntityProperty 287 | if v.json_name in json_names: 288 | raise EntityModelException("%s defines the json property %s more than once" % (name, v.json_name)) 289 | json_names.add(v.json_name) 290 | 291 | body["_properties"] = prop_dict 292 | 293 | # Create the class 294 | klass = super(BaseEntityMetaType, mcs).__new__(mcs, name, bases, body) 295 | 296 | return klass 297 | 298 | 299 | def collection_to_json(collection): 300 | result = [] 301 | for item in collection: 302 | if isinstance(item, BaseAbstractEntity): 303 | result.append(item.get_json_data()) 304 | elif isinstance(item, (list, tuple)): 305 | sub_result = [] 306 | for sub_item in item: 307 | if isinstance(sub_item, BaseAbstractEntity): 308 | sub_result.append(sub_item.get_json_data()) 309 | result.append(sub_result) 310 | elif isinstance(item, dict): 311 | sub_result = {} 312 | for k, v in item: 313 | if isinstance(v, BaseAbstractEntity): 314 | sub_result[k] = v.get_json_data() 315 | result.append(sub_result) 316 | 317 | return json.dumps(result) 318 | 319 | 320 | @add_metaclass(BaseEntityMetaType) 321 | class BaseEntity(BaseAbstractEntity): 322 | _id = None 323 | 324 | def __init__(self, **values): 325 | self._id = values.pop("id", None) 326 | super(BaseEntity, self).__init__(**values) 327 | 328 | @property 329 | def id(self): 330 | """ Get the Id of the entity 331 | 332 | :return: id of entity 333 | """ 334 | return self._id 335 | 336 | def __eq__(self, other): 337 | if not isinstance(other, BaseAbstractEntity): # pragma: no cover 338 | return False 339 | 340 | return self._id == other.id 341 | 342 | def __ne__(self, other): # pragma: no cover 343 | return not self.__eq__(other) 344 | 345 | def get_json_data(self): 346 | """ 347 | Get the Dict of the properties 348 | 349 | :return: dict of properties 350 | """ 351 | result = {} 352 | for name, prop in self._properties.items(): 353 | item = getattr(self, name, None) 354 | if item is not None: 355 | if isinstance(item, BaseAbstractEntity): 356 | item = item.get_json_data() 357 | elif item is not None and isinstance(item, (tuple, list)): 358 | tmp = [] 359 | for i in item: 360 | if isinstance(i, BaseAbstractEntity): 361 | tmp.append(i.get_json_data()) 362 | else: 363 | tmp.append(i) 364 | item = tmp 365 | elif item is not None and isinstance(item, dict): 366 | tmp = {} 367 | for k, v in item.items(): 368 | if isinstance(v, BaseAbstractEntity): 369 | tmp[k] = v.get_json_data() 370 | else: 371 | tmp[k] = v 372 | item = tmp 373 | if item is not None or prop.include_if_null: 374 | result[prop.json_name] = item 375 | if self.id is not None: 376 | result["id"] = self.id 377 | return result 378 | 379 | 380 | __all__ = ["BaseEntity", "BaseAbstractEntity", "BaseEntityMetaType", "collection_to_json", "EntityModelException", "JsonProperty"] 381 | -------------------------------------------------------------------------------- /wiremock/base/base_resource.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | from urllib.parse import urljoin 4 | 5 | import requests 6 | from requests import exceptions as rexc 7 | 8 | from wiremock.base.base_entity import BaseAbstractEntity 9 | from wiremock.constants import Config, logger, make_headers 10 | from wiremock.exceptions import * 11 | 12 | 13 | class RestClient(object): 14 | def __init__( 15 | self, timeout=None, base_url=None, requests_verify=None, requests_cert=None 16 | ): 17 | self.timeout = timeout 18 | self.base_url = base_url 19 | self.requests_verify = requests_verify 20 | self.requests_cert = requests_cert 21 | 22 | def _base_url(self): 23 | return self.base_url or Config.base_url 24 | 25 | def _timeout(self): 26 | return self.timeout or Config.timeout 27 | 28 | def _requests_verify(self): 29 | return self.requests_verify or Config.requests_verify 30 | 31 | def _requests_cert(self): 32 | return self.requests_cert or Config.requests_cert 33 | 34 | def _log(self, action, url, **kwargs): 35 | ctx = {"timeout": kwargs.get("timeout")} 36 | logger.debug( 37 | "%s [%s] - %s", 38 | action, 39 | url, 40 | kwargs.get("json", json.dumps(kwargs.get("data", None))), 41 | extra=ctx, 42 | ) 43 | 44 | def _get_url(self, *uri_parts: List[str]) -> str: 45 | 46 | uri = "/".join(map(lambda x: str(x).rstrip("/"), uri_parts)) 47 | return f"{self._base_url().rstrip('/')}{uri}" 48 | 49 | def post(self, uri, **kwargs): 50 | if "timeout" not in kwargs: 51 | kwargs["timeout"] = self._timeout() 52 | if "requests_verify" not in kwargs: 53 | kwargs["verify"] = self._requests_verify() 54 | if "requests_cert" not in kwargs: 55 | kwargs["cert"] = self._requests_cert() 56 | try: 57 | url = self._get_url(uri) 58 | self._log("POST", url, **kwargs) 59 | return requests.post(url, **kwargs) 60 | except rexc.Timeout as e: # pragma: no cover 61 | raise TimeoutException(-1, e) 62 | except rexc.ConnectionError as e: # pragma: no cover 63 | raise ApiUnavailableException(-1, e) 64 | 65 | def get(self, uri, **kwargs): 66 | if "timeout" not in kwargs: 67 | kwargs["timeout"] = self._timeout() 68 | if "requests_verify" not in kwargs: 69 | kwargs["verify"] = self._requests_verify() 70 | if "requests_cert" not in kwargs: 71 | kwargs["cert"] = self._requests_cert() 72 | try: 73 | url = self._get_url(uri) 74 | self._log("GET", url, **kwargs) 75 | return requests.get(url, **kwargs) 76 | except rexc.Timeout as e: # pragma: no cover 77 | raise TimeoutException(-1, e) 78 | except rexc.ConnectionError as e: # pragma: no cover 79 | raise ApiUnavailableException(-1, e) 80 | 81 | def put(self, uri, **kwargs): 82 | if "timeout" not in kwargs: 83 | kwargs["timeout"] = self._timeout() 84 | if "requests_verify" not in kwargs: 85 | kwargs["verify"] = self._requests_verify() 86 | if "requests_cert" not in kwargs: 87 | kwargs["cert"] = self._requests_cert() 88 | try: 89 | url = self._get_url(uri) 90 | self._log("PUT", url, **kwargs) 91 | return requests.put(url, **kwargs) 92 | except rexc.Timeout as e: # pragma: no cover 93 | raise TimeoutException(-1, e) 94 | except rexc.ConnectionError as e: # pragma: no cover 95 | raise ApiUnavailableException(-1, e) 96 | 97 | def patch(self, uri, **kwargs): # pragma: no cover 98 | if "timeout" not in kwargs: 99 | kwargs["timeout"] = self._timeout() 100 | if "requests_verify" not in kwargs: 101 | kwargs["verify"] = self._requests_verify() 102 | if "requests_cert" not in kwargs: 103 | kwargs["cert"] = self._requests_cert() 104 | try: 105 | url = self._get_url(uri) 106 | self._log("PATCH", url, **kwargs) 107 | return requests.patch(url, **kwargs) 108 | except rexc.Timeout as e: # pragma: no cover 109 | raise TimeoutException(-1, e) 110 | except rexc.ConnectionError as e: # pragma: no cover 111 | raise ApiUnavailableException(-1, e) 112 | 113 | def delete(self, uri, **kwargs): 114 | if "timeout" not in kwargs: 115 | kwargs["timeout"] = self._timeout() 116 | if "requests_verify" not in kwargs: 117 | kwargs["verify"] = self._requests_verify() 118 | if "requests_cert" not in kwargs: 119 | kwargs["cert"] = self._requests_cert() 120 | try: 121 | url = self._get_url(uri) 122 | self._log("DELETE", url, **kwargs) 123 | return requests.delete(url, **kwargs) 124 | except rexc.Timeout as e: # pragma: no cover 125 | raise TimeoutException(-1, e) 126 | except rexc.ConnectionError as e: # pragma: no cover 127 | raise ApiUnavailableException(-1, e) 128 | 129 | def options(self, uri, **kwargs): # pragma: no cover 130 | if "timeout" not in kwargs: 131 | kwargs["timeout"] = self._timeout() 132 | if "requests_verify" not in kwargs: 133 | kwargs["verify"] = self._requests_verify() 134 | if "requests_cert" not in kwargs: 135 | kwargs["cert"] = self._requests_cert() 136 | try: 137 | url = self._get_url(uri) 138 | self._log("OPTIONS", url, **kwargs) 139 | return requests.options(url, **kwargs) 140 | except rexc.Timeout as e: # pragma: no cover 141 | raise TimeoutException(-1, e) 142 | except rexc.ConnectionError as e: # pragma: no cover 143 | raise ApiUnavailableException(-1, e) 144 | 145 | def head(self, uri, **kwargs): # pragma: no cover 146 | if "timeout" not in kwargs: 147 | kwargs["timeout"] = self._timeout() 148 | if "requests_verify" not in kwargs: 149 | kwargs["verify"] = self._requests_verify() 150 | if "requests_cert" not in kwargs: 151 | kwargs["cert"] = self._requests_cert() 152 | try: 153 | url = self._get_url(uri) 154 | self._log("HEAD", url, **kwargs) 155 | return requests.head(url, **kwargs) 156 | except rexc.Timeout as e: # pragma: no cover 157 | raise TimeoutException(-1, e) 158 | except rexc.ConnectionError as e: # pragma: no cover 159 | raise ApiUnavailableException(-1, e) 160 | 161 | @staticmethod 162 | def handle_response(response): 163 | sc = response.status_code 164 | if sc in [200, 201, 204]: 165 | return response 166 | elif sc == 401: # pragma: no cover 167 | raise RequiresLoginException(sc, response.text) 168 | elif sc == 403: # pragma: no cover 169 | raise ForbiddenException(sc, response.text) 170 | elif sc == 404: # pragma: no cover 171 | raise NotFoundException(sc, response.text) 172 | elif sc == 422: # pragma: no cover 173 | raise InvalidInputException(sc, response.text) 174 | elif 200 < sc < 400: # pragma: no cover 175 | raise UnexpectedResponseException(sc, response.text) 176 | elif 400 <= sc < 500 and sc != 404: # pragma: no cover 177 | raise ClientException(sc, response.text) 178 | elif 500 <= sc < 600: # pragma: no cover 179 | raise ServerException(sc, response.text) 180 | else: # pragma: no cover 181 | raise ApiException(sc, response.text) 182 | 183 | 184 | class BaseResource(object): 185 | 186 | REST_CLIENT = RestClient() 187 | 188 | @classmethod 189 | def endpoint(cls): 190 | return "/" # pragma: no cover 191 | 192 | @classmethod 193 | def endpoint_single(cls): 194 | return "/{id}" # pragma: no cover 195 | 196 | @classmethod 197 | def entity_class(cls): 198 | return None # pragma: no cover 199 | 200 | @classmethod 201 | def get_base_uri(cls, endpoint, **id_dict): 202 | if id_dict: 203 | return endpoint.format(**id_dict) 204 | return endpoint 205 | 206 | @staticmethod 207 | def get_entity_id(entity_id, entityClass): 208 | if not ( 209 | isinstance(entity_id, (int, str)) or isinstance(entity_id, entityClass) 210 | ): 211 | raise InvalidInputException(422, entity_id) 212 | if isinstance(entity_id, entityClass): 213 | entity_id = entity_id.id 214 | 215 | return entity_id 216 | 217 | @staticmethod 218 | def validate_is_entity(entity, entityClass): 219 | if not isinstance(entity, entityClass): 220 | raise InvalidInputException(422, entity) 221 | 222 | @classmethod 223 | def _create(cls, entity, parameters=None, ids={}): # pragma: no cover 224 | if isinstance(entity, BaseAbstractEntity): 225 | response = cls.REST_CLIENT.post( 226 | cls.get_base_uri(cls.endpoint(), **ids), 227 | json=entity.get_json_data(), 228 | headers=make_headers(), 229 | params=parameters, 230 | ) 231 | else: 232 | response = cls.REST_CLIENT.post( 233 | cls.get_base_uri(cls.endpoint(), **ids), 234 | data=json.dumps(entity), 235 | headers=make_headers(), 236 | params=parameters, 237 | ) 238 | 239 | response = cls.REST_CLIENT.handle_response(response) 240 | 241 | if cls.entity_class() is None or not issubclass( 242 | cls.entity_class(), BaseAbstractEntity 243 | ): 244 | return response # pragma: no cover 245 | else: 246 | return cls.entity_class().from_dict(response.json()) 247 | 248 | @classmethod 249 | def _update(cls, entity, parameters=None, ids={}): # pragma: no cover 250 | entity_id = getattr(entity, "id", None) 251 | if entity_id is not None: 252 | ids["id"] = entity_id 253 | if isinstance(entity, BaseAbstractEntity): 254 | response = cls.REST_CLIENT.put( 255 | cls.get_base_uri(cls.endpoint_single(), **ids), 256 | json=entity.get_json_data(), 257 | headers=make_headers(), 258 | params=parameters, 259 | ) 260 | else: 261 | response = cls.REST_CLIENT.put( 262 | cls.get_base_uri(cls.endpoint_single(), **ids), 263 | data=json.dumps(entity), 264 | headers=make_headers(), 265 | params=parameters, 266 | ) 267 | 268 | response = cls.REST_CLIENT.handle_response(response) 269 | 270 | if cls.entity_class() is None or not issubclass( 271 | cls.entity_class(), BaseAbstractEntity 272 | ): 273 | return response # pragma: no cover 274 | else: 275 | return cls.entity_class().from_dict(response.json()) 276 | 277 | @classmethod 278 | def _partial_update(cls, entity, parameters=None, ids={}): # pragma: no cover 279 | entity_id = getattr(entity, "id", None) 280 | if entity_id is not None: 281 | ids["id"] = entity_id 282 | if isinstance(entity, BaseAbstractEntity): 283 | response = cls.REST_CLIENT.patch( 284 | cls.get_base_uri(cls.endpoint_single(), **ids), 285 | json=entity.get_json_data(), 286 | headers=make_headers(), 287 | params=parameters, 288 | ) 289 | else: 290 | response = cls.REST_CLIENT.patch( 291 | cls.get_base_uri(cls.endpoint_single(), **ids), 292 | data=json.dumps(entity), 293 | headers=make_headers(), 294 | params=parameters, 295 | ) 296 | 297 | response = cls.REST_CLIENT.handle_response(response) 298 | 299 | if cls.entity_class() is None or not issubclass( 300 | cls.entity_class(), BaseAbstractEntity 301 | ): 302 | return response # pragma: no cover 303 | else: 304 | return cls.entity_class().from_dict(response.json()) 305 | 306 | @classmethod 307 | def _retreive_all(cls, parameters=None, ids={}): # pragma: no cover 308 | response = cls.REST_CLIENT.get( 309 | cls.get_base_uri(cls.endpoint(), **ids), 310 | headers=make_headers(), 311 | params=parameters, 312 | ) 313 | response = cls.REST_CLIENT.handle_response(response) 314 | 315 | if cls.entity_class() is None or not issubclass( 316 | cls.entity_class(), BaseAbstractEntity 317 | ): 318 | return response # pragma: no cover 319 | else: 320 | response_json = response.json() 321 | if isinstance(response_json, (tuple, list)): 322 | results = [] 323 | for r in response_json: 324 | if isinstance(r, dict): 325 | results.append(cls.entity_class().from_dict(r)) 326 | return results 327 | else: 328 | return cls.entity_class().from_dict(response.json()) 329 | 330 | @classmethod 331 | def _retreive_one(cls, entity, parameters=None, ids={}): # pragma: no cover 332 | if isinstance(entity, (int, float)): 333 | ids["id"] = entity 334 | response = cls.REST_CLIENT.get( 335 | cls.get_base_uri(cls.endpoint_single(), **ids), 336 | headers=make_headers(), 337 | params=parameters, 338 | ) 339 | elif entity is not None and issubclass(entity, BaseAbstractEntity): 340 | entity_id = getattr(entity, "id", None) 341 | ids["id"] = entity_id 342 | response = cls.REST_CLIENT.get( 343 | cls.get_base_uri(cls.endpoint_single(), **ids), 344 | headers=make_headers(), 345 | params=parameters, 346 | ) 347 | else: 348 | response = cls.REST_CLIENT.get( 349 | cls.get_base_uri(cls.endpoint_single(), **ids), 350 | headers=make_headers(), 351 | params=parameters, 352 | ) 353 | 354 | response = cls.REST_CLIENT.handle_response(response) 355 | 356 | if cls.entity_class() is None or not issubclass( 357 | cls.entity_class(), BaseAbstractEntity 358 | ): 359 | return response # pragma: no cover 360 | else: 361 | return cls.entity_class().from_dict(response.json()) 362 | 363 | @classmethod 364 | def _delete(cls, entity, parameters=None, ids={}): # pragma: no cover 365 | if isinstance(entity, (int, float)): 366 | ids["id"] = entity 367 | response = cls.REST_CLIENT.delete( 368 | cls.get_base_uri(cls.endpoint_single(), **ids), 369 | headers=make_headers(), 370 | params=parameters, 371 | ) 372 | elif isinstance(entity, BaseAbstractEntity): 373 | entity_id = getattr(entity, "id", None) 374 | ids["id"] = entity_id 375 | response = cls.REST_CLIENT.delete( 376 | cls.get_base_uri(cls.endpoint_single(), **ids), 377 | headers=make_headers(), 378 | params=parameters, 379 | ) 380 | else: 381 | response = cls.REST_CLIENT.delete( 382 | cls.get_base_uri(cls.endpoint_single(), **ids), 383 | headers=make_headers(), 384 | params=parameters, 385 | ) 386 | 387 | response = cls.REST_CLIENT.handle_response(response) 388 | if response is None: 389 | return entity 390 | 391 | if cls.entity_class() is None or not issubclass( 392 | cls.entity_class(), BaseAbstractEntity 393 | ): 394 | return response # pragma: no cover 395 | else: 396 | try: 397 | return cls.entity_class().from_dict(response.json()) 398 | except ValueError: 399 | return response # pragma: no cover 400 | 401 | 402 | __all__ = ["RestClient", "BaseResource"] 403 | -------------------------------------------------------------------------------- /wiremock/client.py: -------------------------------------------------------------------------------- 1 | # import Exceptions 2 | from .exceptions import * 3 | 4 | # import Models 5 | from .resources.settings import GlobalSetting 6 | from .resources.mappings import ( 7 | Mapping, 8 | MappingResponse, 9 | MappingRequest, 10 | DelayDistribution, 11 | ResponseFaultType, 12 | DelayDistributionMethods, 13 | BasicAuthCredentials, 14 | WireMockMatchers, 15 | HttpMethods, 16 | CommonHeaders, 17 | MappingMeta, 18 | AllMappings, 19 | ) 20 | from .resources.requests import ( 21 | RequestResponse, 22 | RequestResponseDefinition, 23 | RequestResponseRequest, 24 | RequestCountResponse, 25 | RequestResponseAll, 26 | RequestResponseFindResponse, 27 | RequestResponseAllMeta, 28 | ) 29 | from .resources.near_misses import ( 30 | NearMissMatchResponse, 31 | NearMissMatchRequest, 32 | NearMissMatchResult, 33 | NearMissRequestPatternResult, 34 | NearMissMatch, 35 | NearMissMatchPatternRequest, 36 | ) 37 | 38 | # import Resources 39 | from .resources.settings.resource import GlobalSettings 40 | from .resources.mappings.resource import Mappings 41 | from .resources.requests.resource import Requests 42 | from .resources.near_misses.resource import NearMisses 43 | from .resources.scenarios.resource import Scenarios 44 | -------------------------------------------------------------------------------- /wiremock/constants.py: -------------------------------------------------------------------------------- 1 | from calendar import timegm 2 | from copy import deepcopy 3 | import logging 4 | 5 | from wiremock import __version__ 6 | from wiremock._compat import add_metaclass 7 | 8 | 9 | logger = logging.getLogger("wiremock") 10 | 11 | 12 | class Singleton(type): 13 | _instances = {} 14 | 15 | def __call__(cls, *args, **kwargs): 16 | if cls not in cls._instances: 17 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 18 | return cls._instances[cls] 19 | 20 | 21 | DEFAULT_TIMEOUT = 30 22 | DEFAULT_BASE_URL = "http://localhost/__admin" 23 | USER_AGENT = "python_wiremock/%s".format(__version__) 24 | DEFAULT_HEADERS = {"Accept": "application/json", "Content-Type": "application/json", "user-agent": USER_AGENT} 25 | DEFAULT_REQUESTS_VERIFY = True 26 | DEFAULT_REQUESTS_CERT = None 27 | 28 | 29 | @add_metaclass(Singleton) 30 | class Config(object): 31 | timeout = DEFAULT_TIMEOUT 32 | base_url = DEFAULT_BASE_URL 33 | user_agent = USER_AGENT 34 | headers = DEFAULT_HEADERS 35 | requests_verify = DEFAULT_REQUESTS_VERIFY 36 | requests_cert = DEFAULT_REQUESTS_CERT 37 | 38 | 39 | Config() # pre-call once 40 | 41 | 42 | def make_headers(**kwargs): 43 | headers = deepcopy(Config.headers) 44 | for key, value in kwargs.items(): 45 | headers[key] = value 46 | return headers 47 | 48 | 49 | def datetime_to_ms(dt): 50 | if isinstance(dt, int): 51 | return dt 52 | else: 53 | tmp = timegm(dt.utctimetuple()) 54 | tmp += float(dt.microsecond) / 1000000.0 55 | return int(tmp * 1000.0) 56 | 57 | 58 | __all__ = ["Config", "make_headers", "logger", "datetime_to_ms"] 59 | -------------------------------------------------------------------------------- /wiremock/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | from .api_unavailable_exception import ApiUnavailableException 3 | from .client_exception import ClientException 4 | from .forbidden_exception import ForbiddenException 5 | from .invalid_input_exception import InvalidInputException 6 | from .not_found_exception import NotFoundException 7 | from .requires_login_exception import RequiresLoginException 8 | from .server_exception import ServerException 9 | from .timeout_exception import TimeoutException 10 | from .unexpected_response_exception import UnexpectedResponseException 11 | -------------------------------------------------------------------------------- /wiremock/exceptions/api_exception.py: -------------------------------------------------------------------------------- 1 | class ApiException(Exception): 2 | """ 3 | Generic Catch-all for API Exceptions. 4 | """ 5 | 6 | status_code = None 7 | 8 | def __init__(self, status_code=None, msg=None, *args, **kwargs): 9 | self.status_code = status_code 10 | super(ApiException, self).__init__(msg, *args, **kwargs) 11 | -------------------------------------------------------------------------------- /wiremock/exceptions/api_unavailable_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class ApiUnavailableException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/client_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class ClientException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/forbidden_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class ForbiddenException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/invalid_input_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class InvalidInputException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/not_found_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class NotFoundException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/requires_login_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class RequiresLoginException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/server_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class ServerException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/timeout_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class TimeoutException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/exceptions/unexpected_response_exception.py: -------------------------------------------------------------------------------- 1 | from .api_exception import ApiException 2 | 3 | 4 | class UnexpectedResponseException(ApiException): 5 | pass 6 | -------------------------------------------------------------------------------- /wiremock/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/wiremock/resources/__init__.py -------------------------------------------------------------------------------- /wiremock/resources/mappings/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import Mapping, MappingResponse, MappingRequest, DelayDistribution, ResponseFaultType, \ 2 | DelayDistributionMethods, BasicAuthCredentials, WireMockMatchers, HttpMethods, CommonHeaders, \ 3 | MappingMeta, AllMappings 4 | -------------------------------------------------------------------------------- /wiremock/resources/mappings/models.py: -------------------------------------------------------------------------------- 1 | from wiremock._compat import add_metaclass 2 | from wiremock.base import ( 3 | BaseAbstractEntity, 4 | BaseEntity, 5 | BaseEntityMetaType, 6 | JsonProperty, 7 | ) 8 | 9 | 10 | class HttpMethods(object): 11 | ANY = "ANY" 12 | GET = "GET" 13 | POST = "POST" 14 | PUT = "PUT" 15 | PATCH = "PATCH" 16 | DELETE = "DELETE" 17 | OPTIONS = "OPTIONS" 18 | HEAD = "HEAD" 19 | 20 | 21 | class CommonHeaders(object): 22 | ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin" 23 | ACCEPT = "Accept" 24 | ACCEPT_CHARSET = "Accept-Charset" 25 | ACCEPT_ENCODING = "Accept-Encoding" 26 | ACCEPT_LANGUAGE = "Accept-Language" 27 | ACCEPT_DATETIME = "Accept-Datetime" 28 | ACCEPT_PATCH = "Accept-Patch" 29 | ACCEPT_RANGES = "Accept-Ranges" 30 | AGE = "Age" 31 | ALLOW = "Allow" 32 | AUTHORIZATION = "Authorization" 33 | CACHE_CONTROL = "Cache-Control" 34 | CONNECTION = "Connection" 35 | COOKIE = "Cookie" 36 | CONTENT_LENGTH = "Content-Length" 37 | CONTENT_MD5 = "Content-MD5" 38 | CONTENT_TYPE = "Content-Type" 39 | DATE = "Date" 40 | DNT = "DNT" 41 | ETAG = "ETag" 42 | EXPECT = "Expect" 43 | EXPIRES = "Expires" 44 | FORWARDED = "Forwarded" 45 | FROM = "From" 46 | HOST = "Host" 47 | IF_MATCH = "If-Match" 48 | IF_MODIFIED_SINCE = "If-Modified-Since" 49 | IF_NONE_MATCH = "If-None-Match" 50 | IF_RANGE = "If-Range" 51 | IF_UNMODIFIED_SINCE = "If-Unmodified-Since" 52 | LAST_MODIFIED = "Last-Modified" 53 | LINK = "Link" 54 | LOCATION = "Location" 55 | MAX_FORWARDS = "Max-Forwards" 56 | ORIGIN = "Origin" 57 | PRAGMA = "Pragma" 58 | PROXY_AUTHORIZATION = "Proxy-Authorization" 59 | PROXY_CONNECTION = "Proxy-Connection" 60 | RANGE = "Range" 61 | REFERER = "Referer" 62 | SERVER = "Server" 63 | SET_COOKIE = "Set-Cookie" 64 | TE = "TE" 65 | USER_AGENT = "user-agent" 66 | UPGRADE = "Upgrade" 67 | VIA = "Via" 68 | WARNING = "Warning" 69 | X_CSRF_TOKEN = "X-Csrf-TOken" 70 | X_FORWARDED_FOR = "X-Forwarded-For" 71 | X_FORWARDED_HOST = "X-Forwarded-Host" 72 | X_FORWARDED_PROTO = "X-Forwarded-Proto" 73 | X_REQUEST_ID = "X-Request-ID" 74 | X_CORRELATION_ID = "X-Correlation-ID" 75 | X_REQUESTED_WITH = "X-Requested-With" 76 | 77 | 78 | class WireMockMatchers(object): 79 | ABSENT = "absent" 80 | ANYTHING = "anything" 81 | CASE_INSENSITIVE = "caseInsensitive" # Should be true/false 82 | CONTAINS = "contains" 83 | DOES_NOT_MATCH = "doesNotMatch" 84 | EQUAL_TO = "equalTo" 85 | EQUAL_TO_JSON = "equalToJson" 86 | EQUAL_TO_XML = "equalToXml" 87 | IGNORE_ARRAY_ORDER = "ignoreArrayOrder" # Should be true/false 88 | IGNORE_EXTRA_ELEMENTS = "ignoreExtraElements" # Should be true/false 89 | MATCHES = "matches" 90 | MATCHES_JSON_PATH = "matchesJsonPath" 91 | MATCHES_X_PATH = "matchesXPath" 92 | X_PATH_NAMESPACES = "xPathNamespaces" 93 | 94 | 95 | @add_metaclass(BaseEntityMetaType) 96 | class BasicAuthCredentials(BaseAbstractEntity): 97 | username = JsonProperty("username") 98 | password = JsonProperty("password") 99 | 100 | 101 | class DelayDistributionMethods(object): 102 | LOG_NORMAL = "lognormal" 103 | UNIFORM = "uniform" 104 | 105 | 106 | class ResponseFaultType(object): 107 | EMPTY_RESPONSE = "EMPTY_RESPONSE" 108 | MALFORMED_RESPONSE_CHUNK = "MALFORMED_RESPONSE_CHUNK" 109 | RANDOM_DATA_THEN_CLOSE = "RANDOM_DATA_THEN_CLOSE" 110 | 111 | 112 | @add_metaclass(BaseEntityMetaType) 113 | class DelayDistribution(BaseAbstractEntity): 114 | distribution_type = JsonProperty("type") 115 | 116 | # lognormal 117 | median = JsonProperty("median") 118 | sigma = JsonProperty("sigma") 119 | 120 | # uniform 121 | upper = JsonProperty("upper") 122 | lower = JsonProperty("lower") 123 | 124 | 125 | @add_metaclass(BaseEntityMetaType) 126 | class MappingRequest(BaseAbstractEntity): 127 | method = JsonProperty("method") 128 | url = JsonProperty("url") 129 | url_path = JsonProperty("urlPath") 130 | url_path_pattern = JsonProperty("urlPathPattern") 131 | url_pattern = JsonProperty("urlPattern") 132 | basic_auth_credentials = JsonProperty( 133 | "basicAuthCredentials", klass=BasicAuthCredentials 134 | ) 135 | cookies = JsonProperty("cookies", klass=dict) 136 | headers = JsonProperty("headers", klass=dict) 137 | query_parameters = JsonProperty("queryParameters", klass=dict) 138 | body_patterns = JsonProperty("bodyPatterns", klass=list, list_klass=dict) 139 | metadata = JsonProperty("metadata", klass=dict) 140 | 141 | 142 | @add_metaclass(BaseEntityMetaType) 143 | class MappingResponse(BaseAbstractEntity): 144 | additional_proxy_request_headers = JsonProperty( 145 | "additionalProxyRequestHeaders", klass=dict 146 | ) 147 | base64_body = JsonProperty("base64Body") 148 | body = JsonProperty("body") 149 | body_file_name = JsonProperty("bodyFileName") 150 | json_body = JsonProperty("jsonBody") 151 | delay_distribution = JsonProperty("delayDistribution", klass=DelayDistribution) 152 | fault = JsonProperty("fault") 153 | fixed_delay_milliseconds = JsonProperty("fixedDelayMilliseconds") 154 | from_configured_stub = JsonProperty("fromConfiguredStub") 155 | headers = JsonProperty("headers", klass=dict) 156 | proxy_base_url = JsonProperty("proxyBaseUrl") 157 | status = JsonProperty("status") 158 | status_message = JsonProperty("statusMessage") 159 | transformer_parameters = JsonProperty("transformerParameters", klass=dict) 160 | transformers = JsonProperty("transformers", klass=list) 161 | metadata = JsonProperty("metadata", klass=dict) 162 | 163 | 164 | class Mapping(BaseEntity): 165 | priority = JsonProperty("priority") 166 | request = JsonProperty("request", klass=MappingRequest) 167 | response = JsonProperty("response", klass=MappingResponse) 168 | persistent = JsonProperty("persistent") 169 | post_serve_actions = JsonProperty("postServeActions", klass=dict) 170 | new_scenario_state = JsonProperty("newScenarioState") 171 | required_scenario_state = JsonProperty("requiredScenarioState") 172 | scenario_name = JsonProperty("scenarioName") 173 | metadata = JsonProperty("metadata", klass=dict) 174 | 175 | 176 | @add_metaclass(BaseEntityMetaType) 177 | class MappingMeta(BaseAbstractEntity): 178 | total = JsonProperty("total") 179 | 180 | 181 | @add_metaclass(BaseEntityMetaType) 182 | class AllMappings(BaseAbstractEntity): 183 | mappings = JsonProperty("mappings", klass=list, list_klass=Mapping) 184 | meta = JsonProperty("meta", klass=MappingMeta) 185 | 186 | 187 | __all__ = [ 188 | "Mapping", 189 | "MappingResponse", 190 | "MappingRequest", 191 | "DelayDistribution", 192 | "ResponseFaultType", 193 | "DelayDistributionMethods", 194 | "BasicAuthCredentials", 195 | "WireMockMatchers", 196 | "HttpMethods", 197 | "CommonHeaders", 198 | "MappingMeta", 199 | "AllMappings", 200 | ] 201 | -------------------------------------------------------------------------------- /wiremock/resources/mappings/resource.py: -------------------------------------------------------------------------------- 1 | from wiremock.base.base_resource import BaseResource 2 | from wiremock.constants import make_headers 3 | from wiremock.resources.mappings import AllMappings, Mapping, MappingResponse 4 | 5 | 6 | class Mappings(BaseResource): 7 | @classmethod 8 | def endpoint(cls): 9 | return "/mappings" 10 | 11 | @classmethod 12 | def endpoint_single(cls): 13 | return "/mappings/{id}" 14 | 15 | @classmethod 16 | def endpoint_delete_by_metadata(cls): 17 | return "/mappings/remove-by-metadata" 18 | 19 | @classmethod 20 | def entity_class(cls): 21 | return MappingResponse 22 | 23 | @classmethod 24 | def create_mapping(cls, mapping, parameters={}): 25 | cls.validate_is_entity(mapping, Mapping) 26 | response = cls.REST_CLIENT.post( 27 | cls.get_base_uri(cls.endpoint()), 28 | json=mapping.get_json_data(), 29 | headers=make_headers(), 30 | params=parameters, 31 | ) 32 | response = cls.REST_CLIENT.handle_response(response) 33 | return Mapping.from_dict(response.json()) 34 | 35 | @classmethod 36 | def retrieve_all_mappings(cls, parameters={}): 37 | response = cls.REST_CLIENT.get( 38 | cls.get_base_uri(cls.endpoint()), headers=make_headers(), params=parameters 39 | ) 40 | response = cls.REST_CLIENT.handle_response(response) 41 | return AllMappings.from_dict(response.json()) 42 | 43 | @classmethod 44 | def retrieve_mapping(cls, mapping_id, parameters={}): 45 | mapping_id = cls.get_entity_id(mapping_id, Mapping) 46 | ids = {"id": mapping_id} 47 | response = cls.REST_CLIENT.get( 48 | cls.get_base_uri(cls.endpoint_single(), **ids), 49 | headers=make_headers(), 50 | params=parameters, 51 | ) 52 | response = cls.REST_CLIENT.handle_response(response) 53 | return Mapping.from_dict(response.json()) 54 | 55 | @classmethod 56 | def update_mapping(cls, mapping, parameters={}): 57 | cls.validate_is_entity(mapping, Mapping) 58 | mapping_id = cls.get_entity_id(mapping, Mapping) 59 | ids = {"id": mapping_id} 60 | response = cls.REST_CLIENT.put( 61 | cls.get_base_uri(cls.endpoint_single(), **ids), 62 | json=mapping.get_json_data(), 63 | headers=make_headers(), 64 | params=parameters, 65 | ) 66 | response = cls.REST_CLIENT.handle_response(response) 67 | return Mapping.from_dict(response.json()) 68 | 69 | @classmethod 70 | def persist_mappings(cls, parameters={}): 71 | ids = {"id": "save"} 72 | response = cls.REST_CLIENT.post( 73 | cls.get_base_uri(cls.endpoint_single(), **ids), 74 | headers=make_headers(), 75 | params=parameters, 76 | ) 77 | return cls.REST_CLIENT.handle_response(response) 78 | 79 | @classmethod 80 | def reset_mappings(cls, parameters={}): 81 | ids = {"id": "reset"} 82 | response = cls.REST_CLIENT.post( 83 | cls.get_base_uri(cls.endpoint_single(), **ids), 84 | headers=make_headers(), 85 | params=parameters, 86 | ) 87 | return cls.REST_CLIENT.handle_response(response) 88 | 89 | @classmethod 90 | def delete_all_mappings(cls, parameters={}): 91 | response = cls.REST_CLIENT.delete( 92 | cls.get_base_uri(cls.endpoint()), headers=make_headers(), params=parameters 93 | ) 94 | return cls.REST_CLIENT.handle_response(response) 95 | 96 | @classmethod 97 | def delete_mapping(cls, mapping_id, parameters={}): 98 | mapping_id = cls.get_entity_id(mapping_id, Mapping) 99 | ids = {"id": mapping_id} 100 | response = cls.REST_CLIENT.delete( 101 | cls.get_base_uri(cls.endpoint_single(), **ids), 102 | headers=make_headers(), 103 | params=parameters, 104 | ) 105 | return cls.REST_CLIENT.handle_response(response) 106 | 107 | @classmethod 108 | def delete_mapping_by_metadata(cls, metadata, parameters={}): 109 | response = cls.REST_CLIENT.post( 110 | cls.get_base_uri(cls.endpoint_delete_by_metadata()), 111 | headers=make_headers(), 112 | params=parameters, 113 | json=metadata, 114 | ) 115 | 116 | return cls.REST_CLIENT.handle_response(response) 117 | 118 | 119 | __all__ = ["Mappings"] 120 | -------------------------------------------------------------------------------- /wiremock/resources/near_misses/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import NearMissMatchResponse, NearMissMatchRequest, NearMissMatchResult, NearMissRequestPatternResult, \ 2 | NearMissMatch, NearMissMatchPatternRequest 3 | -------------------------------------------------------------------------------- /wiremock/resources/near_misses/models.py: -------------------------------------------------------------------------------- 1 | from wiremock._compat import add_metaclass 2 | from wiremock.base import JsonProperty, BaseAbstractEntity, BaseEntityMetaType 3 | from wiremock.resources.mappings.models import BasicAuthCredentials 4 | 5 | 6 | @add_metaclass(BaseEntityMetaType) 7 | class NearMissMatchRequest(BaseAbstractEntity): 8 | url = JsonProperty("url") 9 | absolute_url = JsonProperty("absoluteUrl") 10 | method = JsonProperty("method") 11 | client_ip = JsonProperty("clientIp") 12 | headers = JsonProperty("headers", klass=dict) 13 | query_parameters = JsonProperty("queryParameters", klass=dict) 14 | cookies = JsonProperty("cookies", klass=dict) 15 | basic_auth_credentials = JsonProperty("basicAuthCredentials", klass=BasicAuthCredentials) 16 | browser_proxy_request = JsonProperty("browserProxyRequest") # type: bool 17 | body_as_base64 = JsonProperty("bodyAsBase64") 18 | body = JsonProperty("body") 19 | logged_date = JsonProperty("loggedDate") # epoch seconds 20 | logged_date_string = JsonProperty("loggedDateString") 21 | 22 | 23 | @add_metaclass(BaseEntityMetaType) 24 | class NearMissMatchPatternRequest(BaseAbstractEntity): 25 | url = JsonProperty("url") 26 | url_pattern = JsonProperty("urlPattern") 27 | url_path = JsonProperty("urlPath") 28 | url_path_pattern = JsonProperty("urlPathPattern") 29 | method = JsonProperty("method") 30 | client_ip = JsonProperty("clientIp") 31 | headers = JsonProperty("headers", klass=dict) 32 | query_parameters = JsonProperty("queryParameters", klass=dict) 33 | cookies = JsonProperty("cookies", klass=dict) 34 | body_patterns = JsonProperty("bodyPatterns", klass=dict) 35 | basic_auth_credentials = JsonProperty("basicAuthCredentials", klass=BasicAuthCredentials) 36 | browser_proxy_request = JsonProperty("browserProxyRequest") # type: bool 37 | logged_date = JsonProperty("loggedDate") # epoch seconds 38 | logged_date_string = JsonProperty("loggedDateString") 39 | 40 | 41 | # Responses 42 | 43 | 44 | @add_metaclass(BaseEntityMetaType) 45 | class NearMissRequestPatternResult(BaseAbstractEntity): 46 | url = JsonProperty("url") 47 | absolute_url = JsonProperty("absoluteUrl") 48 | method = JsonProperty("method") 49 | client_ip = JsonProperty("clientIp") 50 | headers = JsonProperty("headers", klass=dict) 51 | query_parameters = JsonProperty("queryParameters", klass=dict) 52 | cookies = JsonProperty("cookies", klass=dict) 53 | basic_auth_credentials = JsonProperty("basicAuthCredentials", klass=BasicAuthCredentials) 54 | browser_proxy_request = JsonProperty("browserProxyRequest") # type: bool 55 | body_as_base64 = JsonProperty("bodyAsBase64") 56 | body = JsonProperty("body") 57 | 58 | 59 | @add_metaclass(BaseEntityMetaType) 60 | class NearMissMatchResult(BaseAbstractEntity): 61 | distance = JsonProperty("distance") # type: float 62 | 63 | 64 | @add_metaclass(BaseEntityMetaType) 65 | class NearMissMatch(BaseAbstractEntity): 66 | request = JsonProperty("request", klass=NearMissMatchRequest) 67 | request_pattern = JsonProperty("requestPattern", klass=NearMissRequestPatternResult) 68 | match_result = JsonProperty("matchResult", klass=NearMissMatchResult) 69 | 70 | 71 | @add_metaclass(BaseEntityMetaType) 72 | class NearMissMatchResponse(BaseAbstractEntity): 73 | near_misses = JsonProperty("nearMisses", klass=list, list_klass=NearMissMatch) 74 | 75 | 76 | __all__ = [ 77 | "NearMissMatchResponse", 78 | "NearMissMatchRequest", 79 | "NearMissMatchResult", 80 | "NearMissRequestPatternResult", 81 | "NearMissMatch", 82 | "NearMissMatchPatternRequest", 83 | ] 84 | -------------------------------------------------------------------------------- /wiremock/resources/near_misses/resource.py: -------------------------------------------------------------------------------- 1 | from wiremock.base.base_resource import BaseResource 2 | from wiremock.resources.near_misses import NearMissMatchPatternRequest, NearMissMatchRequest, NearMissMatchResponse 3 | 4 | 5 | class NearMisses(BaseResource): 6 | @classmethod 7 | def endpoint(cls): 8 | return "/near-misses" 9 | 10 | @classmethod 11 | def endpoint_single(cls): 12 | return "/near-misses" 13 | 14 | @classmethod 15 | def endpoint_request(cls): 16 | return cls.endpoint() + "/request" 17 | 18 | @classmethod 19 | def endpoint_request_pattern(cls): 20 | return cls.endpoint() + "/request-pattern" 21 | 22 | @classmethod 23 | def entity_class(cls): 24 | return NearMissMatchResponse 25 | 26 | @classmethod 27 | def find_nearest_misses_by_request(cls, request, parameters={}): 28 | cls.validate_is_entity(request, NearMissMatchRequest) 29 | response = cls.REST_CLIENT.post(cls.get_base_uri(cls.endpoint_request()), json=request.get_json_data(), params=parameters) 30 | 31 | response = cls.REST_CLIENT.handle_response(response) 32 | return NearMissMatchResponse.from_dict(response.json()) 33 | 34 | @classmethod 35 | def find_nearest_misses_by_request_pattern(cls, request_pattern, parameters={}): 36 | cls.validate_is_entity(request_pattern, NearMissMatchPatternRequest) 37 | response = cls.REST_CLIENT.post(cls.get_base_uri(cls.endpoint_request_pattern()), json=request_pattern.get_json_data(), params=parameters) 38 | 39 | response = cls.REST_CLIENT.handle_response(response) 40 | return NearMissMatchResponse.from_dict(response.json()) 41 | 42 | 43 | __all__ = ["NearMisses"] 44 | -------------------------------------------------------------------------------- /wiremock/resources/requests/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import RequestResponse, RequestResponseDefinition, RequestResponseRequest, RequestCountResponse, \ 2 | RequestResponseAll, RequestResponseFindResponse, RequestResponseAllMeta 3 | -------------------------------------------------------------------------------- /wiremock/resources/requests/models.py: -------------------------------------------------------------------------------- 1 | from wiremock._compat import add_metaclass 2 | from wiremock.base import BaseEntity, JsonProperty, BaseAbstractEntity, BaseEntityMetaType 3 | from wiremock.resources.mappings.models import BasicAuthCredentials 4 | 5 | 6 | @add_metaclass(BaseEntityMetaType) 7 | class RequestCountResponse(BaseAbstractEntity): 8 | count = JsonProperty("count") 9 | 10 | 11 | @add_metaclass(BaseEntityMetaType) 12 | class RequestResponseRequest(BaseAbstractEntity): 13 | method = JsonProperty("method") 14 | url = JsonProperty("url") 15 | absolute_url = JsonProperty("absoluteUrl") 16 | client_ip = JsonProperty("clientIp") 17 | basic_auth_credentials = JsonProperty("basicAuthCredentials", klass=BasicAuthCredentials) 18 | cookies = JsonProperty("cookies", klass=dict) 19 | headers = JsonProperty("headers", klass=dict) 20 | query_parameters = JsonProperty("queryParameters", klass=dict) 21 | browser_proxy_request = JsonProperty("browserProxyRequest") # should be true/false 22 | body = JsonProperty("body") 23 | body_as_base64 = JsonProperty("bodyAsBase64") 24 | logged_date = JsonProperty("loggedDate") # epoch seconds 25 | logged_date_string = JsonProperty("loggedDateString") 26 | 27 | 28 | @add_metaclass(BaseEntityMetaType) 29 | class RequestResponseDefinition(BaseAbstractEntity): 30 | status = JsonProperty("status") 31 | transformers = JsonProperty("transformers", klass=list) 32 | from_configured_stub = JsonProperty("fromConfiguredStub") # will be true/false 33 | transformer_parameters = JsonProperty("transformerParameters", klass=dict) 34 | 35 | 36 | class RequestResponse(BaseEntity): 37 | request = JsonProperty("request", klass=RequestResponseRequest) 38 | response_definition = JsonProperty("responseDefinition", klass=RequestResponseDefinition) 39 | 40 | 41 | @add_metaclass(BaseEntityMetaType) 42 | class RequestResponseAllMeta(BaseAbstractEntity): 43 | total = JsonProperty("total") 44 | 45 | 46 | @add_metaclass(BaseEntityMetaType) 47 | class RequestResponseFindResponse(BaseAbstractEntity): 48 | requests = JsonProperty("requests", klass=list, list_klass=RequestResponseRequest) 49 | 50 | 51 | @add_metaclass(BaseEntityMetaType) 52 | class RequestResponseAll(BaseAbstractEntity): 53 | requests = JsonProperty("requests", klass=list, list_klass=RequestResponse) 54 | meta = JsonProperty("meta", klass=RequestResponseAllMeta) 55 | request_journal_disabled = JsonProperty("requestJournalDisabled") # should be true/false 56 | 57 | 58 | __all__ = [ 59 | "RequestResponse", 60 | "RequestResponseDefinition", 61 | "RequestResponseRequest", 62 | "RequestCountResponse", 63 | "RequestResponseAll", 64 | "RequestResponseFindResponse", 65 | "RequestResponseAllMeta", 66 | ] 67 | -------------------------------------------------------------------------------- /wiremock/resources/requests/resource.py: -------------------------------------------------------------------------------- 1 | from wiremock.constants import make_headers 2 | from wiremock.base.base_resource import BaseResource 3 | from wiremock.resources.requests import RequestCountResponse, RequestResponse, RequestResponseAll, RequestResponseFindResponse 4 | from wiremock.resources.near_misses import NearMissMatchPatternRequest, NearMissMatchResponse 5 | 6 | 7 | class Requests(BaseResource): 8 | @classmethod 9 | def endpoint(cls): 10 | return "/requests" 11 | 12 | @classmethod 13 | def endpoint_single(cls): 14 | return "/requests/{id}" 15 | 16 | @classmethod 17 | def endpoint_requests_unmatched(cls): 18 | return cls.endpoint() + "/unmatched" 19 | 20 | @classmethod 21 | def endpoint_request_unmatched_near_misses(cls): 22 | return cls.endpoint_requests_unmatched() + "/near-misses" 23 | 24 | @classmethod 25 | def entity_class(cls): 26 | return RequestResponse 27 | 28 | @classmethod 29 | def get_all_received_requests(cls, limit=None, since=None, parameters={}): 30 | if limit is not None and limit > 0: 31 | parameters["limit"] = limit 32 | if since is not None: 33 | parameters["since"] = since 34 | 35 | response = cls.REST_CLIENT.get(cls.endpoint(), params=parameters, headers=make_headers()) 36 | response = cls.REST_CLIENT.handle_response(response) 37 | 38 | return RequestResponseAll.from_dict(response.json()) 39 | 40 | @classmethod 41 | def get_request(cls, request_id, parameters={}): 42 | ids = {"id": request_id} 43 | response = cls.REST_CLIENT.get(cls.get_base_uri(cls.endpoint_single(), **ids), headers=make_headers(), params=parameters) 44 | response = cls.REST_CLIENT.handle_response(response) 45 | return RequestResponse.from_dict(response.json()) 46 | 47 | @classmethod 48 | def reset_request_journal(cls, parameters={}): 49 | ids = {"id": "reset"} 50 | response = cls.REST_CLIENT.delete(cls.get_base_uri(cls.endpoint()), headers=make_headers(), params=parameters) 51 | return cls.REST_CLIENT.handle_response(response) 52 | 53 | @classmethod 54 | def get_matching_request_count(cls, request, parameters={}): 55 | cls.validate_is_entity(request, NearMissMatchPatternRequest) 56 | ids = {"id": "count"} 57 | response = cls.REST_CLIENT.post( 58 | cls.get_base_uri(cls.endpoint_single(), **ids), json=request.get_json_data(), headers=make_headers(), params=parameters 59 | ) 60 | response = cls.REST_CLIENT.handle_response(response) 61 | return RequestCountResponse.from_dict(response.json()) 62 | 63 | @classmethod 64 | def get_matching_requests(cls, request, parameters={}): 65 | cls.validate_is_entity(request, NearMissMatchPatternRequest) 66 | ids = {"id": "find"} 67 | response = cls.REST_CLIENT.post( 68 | cls.get_base_uri(cls.endpoint_single(), **ids), json=request.get_json_data(), headers=make_headers(), params=parameters 69 | ) 70 | response = cls.REST_CLIENT.handle_response(response) 71 | return RequestResponseFindResponse.from_dict(response.json()) 72 | 73 | @classmethod 74 | def get_unmatched_requests(cls, parameters={}): 75 | response = cls.REST_CLIENT.get(cls.get_base_uri(cls.endpoint_requests_unmatched()), headers=make_headers(), params=parameters) 76 | response = cls.REST_CLIENT.handle_response(response) 77 | 78 | return RequestResponseFindResponse.from_dict(response.json()) 79 | 80 | @classmethod 81 | def get_unmatched_requests_near_misses(cls, parameters={}): 82 | response = cls.REST_CLIENT.get(cls.get_base_uri(cls.endpoint_request_unmatched_near_misses()), headers=make_headers(), params=parameters) 83 | response = cls.REST_CLIENT.handle_response(response) 84 | return NearMissMatchResponse.from_dict(response.json()) 85 | 86 | 87 | __all__ = ["Requests"] 88 | -------------------------------------------------------------------------------- /wiremock/resources/scenarios/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/wiremock/resources/scenarios/__init__.py -------------------------------------------------------------------------------- /wiremock/resources/scenarios/resource.py: -------------------------------------------------------------------------------- 1 | from wiremock.constants import make_headers 2 | from wiremock.base.base_resource import BaseResource 3 | 4 | 5 | class Scenarios(BaseResource): 6 | @classmethod 7 | def endpoint(cls): 8 | return "/scenarios/reset" 9 | 10 | @classmethod 11 | def endpoint_single(cls): 12 | return "/scenarios" 13 | 14 | @classmethod 15 | def entity_class(cls): 16 | return None 17 | 18 | @classmethod 19 | def reset_all_scenarios(cls, parameters={}): 20 | response = cls.REST_CLIENT.post(cls.get_base_uri(cls.endpoint()), headers=make_headers(), params=parameters) 21 | return cls.REST_CLIENT.handle_response(response) 22 | 23 | 24 | __all__ = ["Scenarios"] 25 | -------------------------------------------------------------------------------- /wiremock/resources/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import GlobalSetting 2 | -------------------------------------------------------------------------------- /wiremock/resources/settings/models.py: -------------------------------------------------------------------------------- 1 | from wiremock._compat import add_metaclass 2 | from wiremock.base import JsonProperty, BaseAbstractEntity, BaseEntityMetaType 3 | 4 | 5 | @add_metaclass(BaseEntityMetaType) 6 | class GlobalSetting(BaseAbstractEntity): 7 | fixed_delay = JsonProperty("fixedDelay") 8 | 9 | 10 | __all__ = ["GlobalSetting"] 11 | -------------------------------------------------------------------------------- /wiremock/resources/settings/resource.py: -------------------------------------------------------------------------------- 1 | from wiremock.base.base_resource import BaseResource 2 | from wiremock.resources.settings import GlobalSetting 3 | 4 | 5 | class GlobalSettings(BaseResource): 6 | @classmethod 7 | def endpoint(cls): 8 | return "/settings" 9 | 10 | @classmethod 11 | def endpoint_single(cls): 12 | return "/settings" 13 | 14 | @classmethod 15 | def entity_class(cls): 16 | return GlobalSetting 17 | 18 | @classmethod 19 | def update_global_settings(cls, settings, parameters={}): 20 | return cls._create(settings, parameters=parameters) 21 | 22 | 23 | __all__ = ["GlobalSettings"] 24 | -------------------------------------------------------------------------------- /wiremock/server/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """WireMock Server Management.""" 3 | from .server import WireMockServer 4 | -------------------------------------------------------------------------------- /wiremock/server/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """WireMockServer Exceptions.""" 3 | 4 | 5 | class WireMockServerException(Exception): 6 | pass 7 | 8 | 9 | class WireMockServerAlreadyStartedError(WireMockServerException): 10 | pass 11 | 12 | 13 | class WireMockServerNotStartedError(WireMockServerException): 14 | pass 15 | -------------------------------------------------------------------------------- /wiremock/server/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """WireMock Server Management.""" 3 | import atexit 4 | import socket 5 | import time 6 | from subprocess import PIPE, STDOUT, Popen 7 | 8 | import requests 9 | from importlib_resources import files 10 | 11 | from wiremock.server.exceptions import ( 12 | WireMockServerAlreadyStartedError, 13 | WireMockServerNotStartedError, 14 | ) 15 | 16 | 17 | class WireMockServer(object): 18 | 19 | DEFAULT_JAVA = "java" # Assume java in PATH 20 | DEFAULT_JAR = files("wiremock") / "server" / "wiremock-standalone-2.35.1.jar" 21 | 22 | def __init__( 23 | self, 24 | java_path=DEFAULT_JAVA, 25 | jar_path=DEFAULT_JAR, 26 | port=None, 27 | max_attempts=10, 28 | root_dir=None, 29 | ): 30 | self.java_path = java_path 31 | self.jar_path = jar_path 32 | self.port = port or self._get_free_port() 33 | self.__subprocess = None 34 | self.__running = False 35 | self.max_attempts = max_attempts 36 | self.root_dir = root_dir 37 | 38 | def __enter__(self): 39 | self.start() 40 | return self 41 | 42 | def __exit__(self, type, value, traceback): 43 | self.stop() 44 | 45 | @property 46 | def is_running(self): 47 | return self.__running 48 | 49 | def start(self): 50 | if self.is_running: 51 | raise WireMockServerAlreadyStartedError( 52 | "WireMockServer already started on port {}".format(self.port) 53 | ) 54 | 55 | cmd = [ 56 | self.java_path, 57 | "-jar", 58 | self.jar_path, 59 | "--port", 60 | str(self.port), 61 | "--local-response-templating", 62 | ] 63 | if self.root_dir is not None: 64 | cmd.append("--root-dir") 65 | cmd.append(str(self.root_dir)) 66 | try: 67 | self.__subprocess = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 68 | except OSError as e: 69 | raise WireMockServerNotStartedError(str(e)) # Problem with Java 70 | 71 | time.sleep(0.1) 72 | if self.__subprocess.poll() is not None: 73 | # Process complete - server not started 74 | raise WireMockServerNotStartedError( 75 | "\n".join( 76 | [ 77 | "returncode: {}".format(self.__subprocess.returncode), 78 | "stdout:", 79 | str(self.__subprocess.stdout.read()), 80 | ] 81 | ) 82 | ) 83 | 84 | # Call the /__admin endpoint as a check for running state 85 | attempts = 0 86 | success = False 87 | while attempts < self.max_attempts: 88 | try: 89 | attempts += 1 90 | resp = requests.get("http://localhost:{}/__admin".format(self.port)) 91 | if resp.status_code == 200: 92 | success = True 93 | break 94 | except requests.exceptions.ConnectionError: 95 | pass 96 | 97 | time.sleep(0.25) 98 | 99 | if not success: 100 | raise WireMockServerNotStartedError( 101 | "unable to get a successful GET http://localhost:{}/__admin response".format( 102 | self.port 103 | ) 104 | ) 105 | 106 | atexit.register(self.stop, raise_on_error=False) 107 | self.__running = True 108 | 109 | def stop(self, raise_on_error=True): 110 | try: 111 | self.__subprocess.kill() 112 | except AttributeError: 113 | if raise_on_error: 114 | raise WireMockServerNotStartedError() 115 | 116 | def _get_free_port(self): 117 | s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) 118 | s.bind(("localhost", 0)) 119 | address, port = s.getsockname() 120 | s.close() 121 | return port 122 | -------------------------------------------------------------------------------- /wiremock/server/wiremock-standalone-2.35.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/wiremock/server/wiremock-standalone-2.35.1.jar -------------------------------------------------------------------------------- /wiremock/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiremock/python-wiremock/6d014e0ab91d2c71586403a6d3881cf1c304aaff/wiremock/testing/__init__.py -------------------------------------------------------------------------------- /wiremock/testing/testcontainer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import tarfile 4 | import tempfile 5 | import time 6 | from contextlib import contextmanager 7 | from pathlib import Path 8 | from typing import Any, Dict, Generator, List, Optional, Tuple, Union 9 | from urllib.parse import urljoin 10 | 11 | import docker 12 | import requests 13 | from testcontainers.core.container import DockerContainer 14 | from testcontainers.core.exceptions import ContainerStartException 15 | from testcontainers.core.waiting_utils import wait_container_is_ready 16 | 17 | from wiremock.resources.mappings.models import Mapping 18 | 19 | TMappingConfigs = Dict[Union[str, Dict], Union[str, int, Dict, Mapping]] 20 | 21 | 22 | class WireMockContainerException(Exception): 23 | pass 24 | 25 | 26 | class WireMockContainer(DockerContainer): 27 | """ 28 | Wiremock container. 29 | """ 30 | 31 | MAPPINGS_DIR: str = "/home/wiremock/mappings/" 32 | FILES_DIR: str = "/home/wiremock/__files/" 33 | 34 | def __init__( 35 | self, 36 | image: str = "wiremock/wiremock:2.35.1-1", 37 | http_server_port: int = 8080, 38 | https_server_port: int = 8443, 39 | secure: bool = True, 40 | verify_ssl_certs: bool = True, 41 | init: bool = True, 42 | docker_client_kwargs: Dict[str, Any] = {}, 43 | ) -> None: 44 | self.http_server_port = http_server_port 45 | self.https_server_port = https_server_port 46 | self.secure = secure 47 | self.verify_ssl_certs = verify_ssl_certs 48 | super(WireMockContainer, self).__init__(image, **docker_client_kwargs) 49 | 50 | if init: 51 | self.initialize() 52 | 53 | def initialize(self) -> None: 54 | self.wire_mock_args: List[str] = [] 55 | self.mapping_stubs: Dict[str, str] = {} 56 | self.mapping_files: Dict[str, str] = {} 57 | self.extensions: Dict[str, bytes] = {} 58 | 59 | if self.secure: 60 | self.with_https_port() 61 | else: 62 | self.with_http_port() 63 | 64 | def with_http_port(self) -> None: 65 | self.with_cli_arg("--port", str(self.http_server_port)) 66 | self.with_exposed_ports(self.http_server_port) 67 | 68 | def with_https_port(self) -> None: 69 | self.with_cli_arg("--https-port", str(self.https_server_port)) 70 | self.with_exposed_ports(self.https_server_port) 71 | 72 | def with_cli_arg(self, arg_name: str, arg_value: str) -> "WireMockContainer": 73 | self.wire_mock_args.append(arg_name) 74 | self.wire_mock_args.append(arg_value) 75 | return self 76 | 77 | def with_mapping(self, name: str, data: TMappingConfigs) -> "WireMockContainer": 78 | self.mapping_stubs[name] = json.dumps(data) 79 | return self 80 | 81 | def with_file(self, name: str, data: Dict[str, Any]): 82 | self.mapping_files[name] = json.dumps(data) 83 | return self 84 | 85 | def with_command(self, cmd: Optional[str] = None) -> "WireMockContainer": 86 | 87 | if not cmd: 88 | cmd = " ".join(self.wire_mock_args) 89 | 90 | super().with_command(cmd) 91 | 92 | return self 93 | 94 | def copy_file_to_container(self, host_path: Path, container_path: Path) -> None: 95 | with open(host_path, "rb") as fp: 96 | self.get_wrapped_container().put_archive( 97 | path=container_path.as_posix(), data=fp.read() 98 | ) 99 | 100 | def copy_files_to_container( 101 | self, configs: Dict[str, Any], container_dir_path: Path, mode: str = "w+" 102 | ) -> None: 103 | 104 | temp_dir = tempfile.mkdtemp() 105 | 106 | # generate temp files all config files 107 | for config_name, config_content in configs.items(): 108 | file_name = os.path.basename(config_name) 109 | destination_path = os.path.join(temp_dir, file_name) 110 | with open(destination_path, mode) as fp: 111 | fp.write(config_content) 112 | 113 | # tar all files from temp dir 114 | tarfile_path = f"{temp_dir}.tar.gz" 115 | with tarfile.open(tarfile_path, "w:gz") as tar: 116 | for root, _, files in os.walk(temp_dir): 117 | for file in files: 118 | file_path = os.path.join(root, file) 119 | arcname = os.path.relpath(file_path, temp_dir) 120 | tar.add(file_path, arcname=arcname) 121 | 122 | # copy tar archive onto container and extract at {container_dir_path} 123 | self.copy_file_to_container( 124 | host_path=Path(tarfile_path), container_path=container_dir_path 125 | ) 126 | 127 | def copy_mappings_to_container(self) -> None: 128 | """Copies all mappings files generated with 129 | `.with_mapping('hello-world.json', {...})` to the container under 130 | the configured MAPPINGS_DIR 131 | """ 132 | 133 | self.copy_files_to_container( 134 | configs=self.mapping_stubs, container_dir_path=Path(f"{self.MAPPINGS_DIR}") 135 | ) 136 | 137 | def copy_mapping_files_to_container(self) -> None: 138 | """Copies all mappings files generated with 139 | `.with_file('hello.json', {...})` to the container under 140 | the configured FILES_DIR 141 | """ 142 | self.copy_files_to_container( 143 | configs=self.mapping_files, container_dir_path=Path(f"{self.FILES_DIR}") 144 | ) 145 | 146 | def server_running(self, retry_count: int = 3, retry_delay: int = 1) -> bool: 147 | """Pings the __admin/mappings endpoint of the wiremock server running inside the 148 | container as a proxy for checking if the server is up and running. 149 | 150 | {retry_count} attempts requests will be made with a delay of {retry_delay} 151 | to allow for race conditions when containers are being spun up 152 | quickly between tests. 153 | 154 | Args: 155 | retry_count: The number of attempts made to ping the server 156 | retry_delay: The number of seconds to wait before each attempt 157 | 158 | Returns: 159 | True if the request is successful 160 | """ 161 | 162 | for _ in range(retry_count): 163 | try: 164 | response = requests.get( 165 | self.get_url("__admin/mappings"), verify=self.verify_ssl_certs 166 | ) 167 | if response.status_code == 200: 168 | return True 169 | except requests.exceptions.RequestException as e: 170 | print(f"Request failed: {e}") 171 | 172 | time.sleep(retry_delay) 173 | 174 | return False 175 | 176 | def reload_mappings(self) -> requests.Response: 177 | """When mappings are mounted into a container via files 178 | the server will already be running as it will start as soon as the container 179 | starts. reload_mappings is called via the rest api to ensure any mappings 180 | added after the server starts are picked up. 181 | """ 182 | resp = requests.post( 183 | self.get_url("__admin/mappings/reset"), verify=self.verify_ssl_certs 184 | ) 185 | if not resp.status_code <= 300: 186 | raise WireMockContainerException("Failed to reload mappings") 187 | 188 | return resp 189 | 190 | @wait_container_is_ready() 191 | def configure(self) -> None: 192 | if not self.server_running(): 193 | raise WireMockContainerException( 194 | "Server does not appear to be running in container" 195 | ) 196 | 197 | self.copy_mappings_to_container() 198 | self.copy_mapping_files_to_container() 199 | 200 | self.reload_mappings() 201 | 202 | def get_base_url(self) -> str: 203 | """Generate the base url of the container wiremock-server 204 | 205 | Returns: 206 | The base to the container based on the hostname and exposed ports 207 | """ 208 | proto = "https" if self.secure else "http" 209 | port = self.https_server_port if self.secure else self.http_server_port 210 | 211 | if os.environ.get("WIREMOCK_DIND", False): 212 | host = "host.docker.internal" 213 | else: 214 | host = self.get_container_host_ip() 215 | 216 | return f"{proto}://{host}:{self.get_exposed_port(port)}" 217 | 218 | def get_url(self, path: str) -> str: 219 | return urljoin(self.get_base_url(), path) 220 | 221 | def start(self, cmd: Optional[str] = None) -> "WireMockContainer": 222 | self.with_command(cmd) 223 | super().start() 224 | self.configure() 225 | return self 226 | 227 | 228 | @contextmanager 229 | def wiremock_container( 230 | image: str = "wiremock/wiremock:2.35.1-1", 231 | http_server_port: int = 8080, 232 | https_server_port: int = 8443, 233 | secure: bool = True, 234 | verify_ssl_certs: bool = True, 235 | mappings: List[Tuple[str, TMappingConfigs]] = [], 236 | start: bool = True, 237 | docker_client_kwargs: Dict[str, Any] = {}, 238 | ) -> Generator[WireMockContainer, None, None]: 239 | """ 240 | Start a wiremock test container using Testcontainers 241 | 242 | Attributes 243 | image (str): specify the docker image name and version for wiremock server. 244 | http_server_port (int): The port of the HTTP server port 245 | https_server_port (int): The port of the HTTPS server port 246 | secure (bool): Set True If you're connecting to the server via ssl. 247 | verify_ssl_certs (bool): Should requests verify ssl certs when using 248 | secure connections. 249 | mappings list[Tuple[str, TMappingConfigs]]: a list of tuples containing 250 | mapping name and mapping dictionary. 251 | start (bool): If true, start the container, otherwise just yield 252 | container instance 253 | docker_client_kwargs (dict): Kwargs to pass to the docker client 254 | 255 | Examples: 256 | 257 | Mappings can be provided as tuples of mapping name, TMappingConfigs. This 258 | will create mapping config files in the container. 259 | 260 | ``` 261 | mappings = [ 262 | ( 263 | "hello-world.json", 264 | { 265 | "request": {"method": "GET", "url": "/hello"}, 266 | "response": {"status": 200, "body": "hello"}, 267 | }, 268 | ) 269 | ] 270 | 271 | with wiremock_container(mappings=mappings, verify_ssl_certs=False) as wm: 272 | 273 | resp1 = requests.get(wm.get_url("/hello"), verify=False) 274 | assert resp1.status_code == 200 275 | ``` 276 | 277 | Or you can use the SDK directly to create mappings via the API. 278 | 279 | 280 | :return: WireMockContainer instance 281 | """ 282 | 283 | client = docker.from_env() 284 | client.ping() 285 | try: 286 | wm = WireMockContainer( 287 | image=image, 288 | http_server_port=http_server_port, 289 | https_server_port=https_server_port, 290 | secure=secure, 291 | verify_ssl_certs=verify_ssl_certs, 292 | docker_client_kwargs=docker_client_kwargs, 293 | ) 294 | [wm.with_mapping(m_name, m_data) for m_name, m_data in mappings] 295 | if start: 296 | with wm: 297 | yield wm 298 | else: 299 | yield wm 300 | except ContainerStartException as e: 301 | raise WireMockContainerException("Error starting wiremock container") from e 302 | except requests.exceptions.RequestException as e: 303 | raise WireMockContainerException( 304 | "Error connecting to wiremock container" 305 | ) from e 306 | finally: 307 | client.close() 308 | --------------------------------------------------------------------------------