diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..99bcbec --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = E226,E302,E41 +max-line-length = 88 +max-complexity = 10 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0426fda..34a32bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | poetry env use ${{matrix.python-version}} - poetry install + poetry install --extras=testing - name: Test with pytest run: | diff --git a/README.md b/README.md index 5d09913..8f24b22 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,86 @@

-This is a python admin API client to a standalone WireMock server. +Python Wiremock is an HTTP client that allows users to interact with a Wiremock instance from within a Python project. [![a](https://img.shields.io/badge/slack-%23wiremock%2Fpython-brightgreen?style=flat&logo=slack)](https://slack.wiremock.org/) [![Coverage Status](https://coveralls.io/repos/github/wiremock/python-wiremock/badge.svg?branch=master)](https://coveralls.io/github/wiremock/python-wiremock?branch=master) [![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg)](http://wiremock.readthedocs.org/) +## Key Features + +WireMock can run in unit tests, as a standalone process or a container. Key features include: + +- Supports most of the major [Wiremock](https://wiremock.org/docs) features (more on their way soon) +- Support for [testcontainers-python](https://github.com/testcontainers/testcontainers-python) to easily start wiremock server for your tests +- Support for standalone wiremock JAVA sever + ## Install as Dependency To install: - pip install wiremock + `pip install wiremock` + +To install with testing dependencies: + + `pip install wiremock[testing]` + +To install via Poetry: + + `poetry add --extras=testing wiremock` + +## Quick Start + +The preferred way of using WireMock to mock your services is by using the provided `WireMockContainer` [testcontainers-python](https://github.com/testcontainers/testcontainers-python). + +```python +import pytest + +from wiremock.testing.testcontainer import wiremock_container + +@pytest.fixture(scope="session") # (1) +def wm_server(): + with wiremock_container(secure=False) as wm: + + Config.base_url = wm.get_url("__admin") # (2) + + Mappings.create_mapping( + Mapping( + request=MappingRequest(method=HttpMethods.GET, url="/hello"), + response=MappingResponse(status=200, body="hello"), + persistent=False, + ) + ) # (3) + yield wm + + +def test_get_hello_world(wm_server): # (4) + + resp1 = requests.get(wm_server.get_url("/hello"), verify=False) + + assert resp1.status_code == 200 + assert resp1.content == b"hello" +``` + +1. Create a pytest fixture to manage the container life-cycle. use fixture `scope` to control how often the container is created + +2. Set the wiremock sdk config url to the url exposed by the container + +3. Create response and request mappings using the Admin SDK. + +4. Use the `wm_server` fixture in your tests and make requests against the mock server. + +You can read more about Testcontainers support in python-wiremock [here](docs/testcontainers.md). + +## Examples + +There are several example projects included to demonstrate the different ways that wiremock can be used to mock +services in your tests and systems. The example test modules demonstrate different strategies for testing against +the same "product service" and act as a good demonstration of real world applications to help you get started. + +- [Python Testcontainers](examples/tests/test_containers.py) + +- [Standlone JAVA Server Version](examples/tests/test_java_server.py) ## Documentation diff --git a/docs/testcontainers.md b/docs/testcontainers.md new file mode 100644 index 0000000..2650b32 --- /dev/null +++ b/docs/testcontainers.md @@ -0,0 +1,141 @@ +# WireMock module for Testcontainers Python + +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. + +## Using the context manager + +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 +used in conjuction with a pytest fixture to easily manage the life-cycle of the container. + +```python +import pytest + +from wiremock.testing.testcontainer import wiremock_container + +@pytest.fixture(scope="session") # (1) +def wm_server(): + with wiremock_container(secure=False) as wm: + + Config.base_url = wm.get_url("__admin") # (2) + + Mappings.create_mapping( + Mapping( + request=MappingRequest(method=HttpMethods.GET, url="/hello"), + response=MappingResponse(status=200, body="hello"), + persistent=False, + ) + ) # (3) + yield wm + + +def test_get_hello_world(wm_server): # (4) + + resp1 = requests.get(wm_server.get_url("/hello"), verify=False) + + assert resp1.status_code == 200 + assert resp1.content == b"hello" +``` + +1. Create a pytest fixture to manage the container life-cycle. use fixture `scope` to control how often the container is created + +2. Set the wiremock sdk config url to the url exposed by the container + +3. Create response and request mappings using the Admin SDK. + +4. Use the `wm_server` fixture in your tests and make requests against the mock server. + +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. + +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. + +The `wiremock_container` also supports generating mapping request and response files for you via the mappings kwarg. + +```python + +@pytest.mark.container_test +def test_configure_via_wiremock_container_context_manager(): + + mappings = [ + ( + "hello-world.json", + { + "request": {"method": "GET", "url": "/hello"}, + "response": {"status": 200, "body": "hello"}, + }, + ) + ] + + with wiremock_container(mappings=mappings, verify_ssl_certs=False) as wm: + + resp1 = requests.get(wm.get_url("/hello"), verify=False) + assert resp1.status_code == 200 + assert resp1.content == b"hello" +``` + +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 +of options. + +## Using the WireMockContainer directly + +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 +if you maintain your request and response stubs as files. + +```python +WireMockContainer(verify_ssl_certs=False) + .with_mapping( + "hello-world.json", + { + "request": {"method": "GET", "url": "/hello"}, + "response": {"status": 200, "body": "hello"}, + }, + ) + .with_mapping( + "hello-world-file.json", + { + "request": {"method": "GET", "url": "/hello2"}, + "response": {"status": 200, "bodyFileName": "hello.json"}, + }, + ) + .with_file("hello.json", {"message": "Hello World !"}) + .with_cli_arg("--verbose", "") + .with_cli_arg("--root-dir", "/home/wiremock") + .with_env("JAVA_OPTS", "-Djava.net.preferIPv4Stack=true") +) +``` + +## Using WireMockContainer inside Docker (dind) + +It's common that you might need to start Testcontainers from inside of another container. The example project in [Testcontainer Example](example/docker-compose.yaml) actually does this. + +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 +will explicitly be set to `host.docker.internal`. + +Let's take a look at the example docker-compose.yaml the example products service uses. + +```yaml +version: "3" + +services: + overview_srv: + build: + context: ../ + dockerfile: example/Dockerfile + ports: + - "5001:5001" + environment: + - WIREMOCK_DIND=true # (1) Set the env var + extra_hosts: + - "host.docker.internal:host-gateway" # (2) + volumes: + - /var/run/docker.sock:/var/run/docker.sock # (3) + - ..:/app/ + - .:/app/example/ + command: uvicorn product_mock.overview_service:app --host=0.0.0.0 --port=5001 +``` + +1. Set the environment variable to instruct WireMockContainer that we're running in `DIND` mode. + +2. Map the host.docker.internal to host-gateway. Docker will magically replace the host-gateway value with the ip of the container. + This mapping is required when using dind on certain CI system like github actions. + +3. Mount the docker binary into the container diff --git a/example/docker-compose.yml b/example/docker-compose.yml index bffd223..7b47e3d 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -7,7 +7,12 @@ services: dockerfile: example/Dockerfile ports: - "5001:5001" + environment: + - WIREMOCK_DIND=true + extra_hosts: + - "host.docker.internal:host-gateway" volumes: + - /var/run/docker.sock:/var/run/docker.sock - ..:/app/ - .:/app/example/ command: uvicorn product_mock.overview_service:app --host=0.0.0.0 --port=5001 diff --git a/example/poetry.lock b/example/poetry.lock index 4146942..409322c 100644 --- a/example/poetry.lock +++ b/example/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "anyio" @@ -62,6 +62,7 @@ mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -193,6 +194,58 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + +[[package]] +name = "docker" +version = "6.1.0" +description = "A Python library for the Docker Engine API." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docker-6.1.0-py3-none-any.whl", hash = "sha256:b65c999f87cb5c31700b6944dc17a631071170d1aab3ad6e23506068579f885d"}, + {file = "docker-6.1.0.tar.gz", hash = "sha256:cb697eccfeff55d232f7a7f4f88cd3770d27327c38d6c266b8f55c9f14a8491e"}, +] + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] + +[[package]] +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, + {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "fastapi" version = "0.95.1" @@ -369,6 +422,7 @@ files = [ [package.dependencies] mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=3.10" [package.extras] @@ -393,7 +447,7 @@ files = [ name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -512,13 +566,39 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "requests" version = "2.29.0" @@ -571,6 +651,52 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "testcontainers" +version = "3.7.1" +description = "Library provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "testcontainers-3.7.1-py2.py3-none-any.whl", hash = "sha256:7f48cef4bf0ccd78f1a4534d4b701a003a3bace851f24eae58a32f9e3f0aeba0"}, +] + +[package.dependencies] +deprecation = "*" +docker = ">=4.0.0" +wrapt = "*" + +[package.extras] +arangodb = ["python-arango"] +azurite = ["azure-storage-blob"] +clickhouse = ["clickhouse-driver"] +docker-compose = ["docker-compose"] +google-cloud-pubsub = ["google-cloud-pubsub (<2)"] +kafka = ["kafka-python"] +keycloak = ["python-keycloak"] +mongo = ["pymongo"] +mssqlserver = ["pymssql"] +mysql = ["pymysql", "sqlalchemy"] +neo4j = ["neo4j"] +oracle = ["cx-Oracle", "sqlalchemy"] +postgresql = ["psycopg2-binary", "sqlalchemy"] +rabbitmq = ["pika"] +redis = ["redis"] +selenium = ["selenium"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "typing-extensions" version = "4.5.0" @@ -619,6 +745,23 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "websocket-client" +version = "1.5.1" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, + {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, +] + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "wiremock" version = "2.3.1" @@ -636,7 +779,92 @@ requests = "^2.20.0" type = "directory" url = ".." +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "66e60a3e2d5f89fce05550057a997fe56c789515caaab18841f7ce3dc9e50679" +python-versions = "^3.10.6" +content-hash = "0d8e5ecb7cd01b6721d9b5298337df25b031d5c358e131bb97ba852b56b0f685" diff --git a/example/product_mock/overview_service.py b/example/product_mock/overview_service.py index 1d36efb..67a6940 100644 --- a/example/product_mock/overview_service.py +++ b/example/product_mock/overview_service.py @@ -14,11 +14,10 @@ async def read_root(): @app.get("/overview") async def read_products(category: str = "", sort_by: str = ""): PRODUCTS_SERVICE_HOST = os.environ.get( - "PRODUCTS_SERVICE_HOST", "http://products_srv" + "PRODUCTS_SERVICE_HOST", "http://products_srv:5002" ) - PRODUCTS_SERVICE_PORT = os.environ.get("PRODUCTS_SERVICE_PORT", 5002) - PRODUCTS_URL = f"{PRODUCTS_SERVICE_HOST}:{PRODUCTS_SERVICE_PORT}/products" + PRODUCTS_URL = f"{PRODUCTS_SERVICE_HOST}/products" params = {} if category != "": diff --git a/example/pyproject.toml b/example/pyproject.toml index 4ab551a..951d18d 100644 --- a/example/pyproject.toml +++ b/example/pyproject.toml @@ -8,13 +8,15 @@ readme = "README.md" packages = [{include = "product_mock"}] [tool.poetry.dependencies] -python = "^3.11" +python = "^3.10.6" fastapi = "^0.95.1" requests = "^2.29.0" uvicorn = "^0.22.0" wiremock = { path = "../", develop=true} httpx = "^0.24.0" importlib-resources = "^5.12.0" +docker = "^6.1.0" +testcontainers = "^3.7.1" [tool.poetry.group.dev.dependencies] pytest = "^7.3.1" diff --git a/example/tests/test_java_server.py b/example/tests/test_java_server.py index 73dd5be..3e1a21f 100644 --- a/example/tests/test_java_server.py +++ b/example/tests/test_java_server.py @@ -2,70 +2,37 @@ import pytest from fastapi.testclient import TestClient -from wiremock.client import (HttpMethods, Mapping, MappingRequest, - MappingResponse, Mappings) +from wiremock.client import Mappings from wiremock.constants import Config from wiremock.server import WireMockServer from product_mock.overview_service import app -client = TestClient(app) +from .utils import get_mappings, get_products +client = TestClient(app) -def get_products(): - return [ - {"name": "Mock Product A", "price": 10.99, "category": "Books"}, - {"name": "Mock Product B", "price": 5.99, "category": "Movies"}, - {"name": "Mock Product C", "price": 7.99, "category": "Electronics"}, - {"name": "Mock Product D", "price": 12.99, "category": "Books"}, - {"name": "Mock Product E", "price": 8.99, "category": "Movies"}, - {"name": "Mock Product F", "price": 15.99, "category": "Electronics"}, - ] +@pytest.fixture(scope="module") +def wm_java(): + with WireMockServer() as _wm: + Config.base_url = f"http://localhost:{_wm.port}/__admin" + os.environ["PRODUCTS_SERVICE_HOST"] = f"http://localhost:{_wm.port}" + [Mappings.create_mapping(mapping=mapping) for mapping in get_mappings()] -@pytest.fixture(scope="session") -def wm(): - with WireMockServer() as wm: - Config.base_url = f"http://localhost:{wm.port}/__admin" - os.environ["PRODUCTS_SERVICE_HOST"] = "http://localhost" - os.environ["PRODUCTS_SERVICE_PORT"] = str(wm.port) - Mappings.create_mapping( - mapping=Mapping( - priority=100, - request=MappingRequest(method=HttpMethods.GET, url="/products"), - response=MappingResponse(status=200, json_body=get_products()), - persistent=False, - ) - ) - Mappings.create_mapping( - mapping=Mapping( - priority=100, - request=MappingRequest( - method=HttpMethods.GET, - url=r"/products?category=Books", - query_parameters={"category": {"equalTo": "Books"}}, - ), - response=MappingResponse( - status=200, - json_body=list( - filter(lambda p: p["category"] == "Books", get_products()) - ), - ), - persistent=False, - ) - ) + yield _wm - yield wm + Mappings.delete_all_mappings() -def test_get_overview_default(wm): +def test_get_overview_default(wm_java): resp = client.get("/overview") assert resp.status_code == 200 assert resp.json() == {"products": get_products()} -def test_get_overview_with_filters(wm): +def test_get_overview_with_filters(wm_java): resp = client.get("/overview?category=Books") assert resp.status_code == 200 diff --git a/example/tests/test_testcontainers.py b/example/tests/test_testcontainers.py new file mode 100644 index 0000000..eb7c1cf --- /dev/null +++ b/example/tests/test_testcontainers.py @@ -0,0 +1,46 @@ +import os + +import pytest +from fastapi.testclient import TestClient +from wiremock.client import Mappings +from wiremock.constants import Config +from wiremock.testing.testcontainer import wiremock_container + +from product_mock.overview_service import app + +from .utils import get_mappings, get_products + +client = TestClient(app) + + +@pytest.fixture(scope="module") +def wm_docker(): + with wiremock_container(verify_ssl_certs=False, secure=False) as wm: + + Config.base_url = wm.get_url("__admin") + + os.environ["PRODUCTS_SERVICE_HOST"] = wm.get_base_url() + + [Mappings.create_mapping(mapping=mapping) for mapping in get_mappings()] + + yield wm + + Mappings.delete_all_mappings() + + +@pytest.mark.usefixtures("wm_docker") +def test_get_overview_default(): + resp = client.get("/overview") + + assert resp.status_code == 200 + assert resp.json() == {"products": get_products()} + + +@pytest.mark.usefixtures("wm_docker") +def test_get_overview_with_filters(): + resp = client.get("/overview?category=Books") + + assert resp.status_code == 200 + assert resp.json() == { + "products": list(filter(lambda p: p["category"] == "Books", get_products())) + } diff --git a/example/tests/utils.py b/example/tests/utils.py new file mode 100644 index 0000000..f2d89eb --- /dev/null +++ b/example/tests/utils.py @@ -0,0 +1,38 @@ +from wiremock.client import HttpMethods, Mapping, MappingRequest, MappingResponse + + +def get_products(): + return [ + {"name": "Mock Product A", "price": 10.99, "category": "Books"}, + {"name": "Mock Product B", "price": 5.99, "category": "Movies"}, + {"name": "Mock Product C", "price": 7.99, "category": "Electronics"}, + {"name": "Mock Product D", "price": 12.99, "category": "Books"}, + {"name": "Mock Product E", "price": 8.99, "category": "Movies"}, + {"name": "Mock Product F", "price": 15.99, "category": "Electronics"}, + ] + + +def get_mappings() -> list[Mapping]: + return [ + Mapping( + priority=100, + request=MappingRequest(method=HttpMethods.GET, url="/products"), + response=MappingResponse(status=200, json_body=get_products()), + persistent=False, + ), + Mapping( + priority=100, + request=MappingRequest( + method=HttpMethods.GET, + url=r"/products?category=Books", + query_parameters={"category": {"equalTo": "Books"}}, + ), + response=MappingResponse( + status=200, + json_body=list( + filter(lambda p: p["category"] == "Books", get_products()) + ), + ), + persistent=False, + ), + ] diff --git a/poetry.lock b/poetry.lock index 4b10465..7888925 100644 --- a/poetry.lock +++ b/poetry.lock @@ -267,6 +267,21 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + [[package]] name = "distlib" version = "0.3.6" @@ -279,6 +294,28 @@ files = [ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] +[[package]] +name = "docker" +version = "6.1.0" +description = "A Python library for the Docker Engine API." +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "docker-6.1.0-py3-none-any.whl", hash = "sha256:b65c999f87cb5c31700b6944dc17a631071170d1aab3ad6e23506068579f885d"}, + {file = "docker-6.1.0.tar.gz", hash = "sha256:cb697eccfeff55d232f7a7f4f88cd3770d27327c38d6c266b8f55c9f14a8491e"}, +] + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] + [[package]] name = "exceptiongroup" version = "1.1.1" @@ -390,7 +427,7 @@ files = [ name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -559,6 +596,30 @@ PyYAML = "*" requests = "*" six = "*" +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "pyyaml" version = "6.0" @@ -665,6 +726,40 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "testcontainers" +version = "3.7.1" +description = "Library provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "testcontainers-3.7.1-py2.py3-none-any.whl", hash = "sha256:7f48cef4bf0ccd78f1a4534d4b701a003a3bace851f24eae58a32f9e3f0aeba0"}, +] + +[package.dependencies] +deprecation = "*" +docker = ">=4.0.0" +wrapt = "*" + +[package.extras] +arangodb = ["python-arango"] +azurite = ["azure-storage-blob"] +clickhouse = ["clickhouse-driver"] +docker-compose = ["docker-compose"] +google-cloud-pubsub = ["google-cloud-pubsub (<2)"] +kafka = ["kafka-python"] +keycloak = ["python-keycloak"] +mongo = ["pymongo"] +mssqlserver = ["pymssql"] +mysql = ["pymysql", "sqlalchemy"] +neo4j = ["neo4j"] +oracle = ["cx-Oracle", "sqlalchemy"] +postgresql = ["psycopg2-binary", "sqlalchemy"] +rabbitmq = ["pika"] +redis = ["redis"] +selenium = ["selenium"] + [[package]] name = "tomli" version = "2.0.1" @@ -844,6 +939,23 @@ files = [ [package.extras] watchmedo = ["PyYAML (>=3.10)"] +[[package]] +name = "websocket-client" +version = "1.5.1" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, + {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, +] + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "wheel" version = "0.40.0" @@ -859,6 +971,91 @@ files = [ [package.extras] test = ["pytest (>=6.0.0)"] +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + [[package]] name = "zipp" version = "3.15.0" @@ -875,7 +1072,10 @@ files = [ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[extras] +testing = ["docker", "testcontainers"] + [metadata] lock-version = "2.0" python-versions = "^3.7 | ^3.8 | ^3.9 | ^3.10 | ^3.11" -content-hash = "d3f2f63f578574ac4285bea3f27c46ebfdf24d1adbe9eb6be4d99b08c4498294" +content-hash = "1e6ca43d5a6349526bc05db2304f5073fe1b66ec12ca3a99f8c949bb4a731afb" diff --git a/pyproject.toml b/pyproject.toml index 1ab8cef..16a12f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wiremock" -version = "2.5.0" +version = "2.6.0-alpha" description = "Wiremock Admin API Client" authors = ["Cody Lee ", "Mike Waites "] license = "OSI Approved :: Apache Software License" @@ -33,6 +33,8 @@ classifiers=[ python = "^3.7 | ^3.8 | ^3.9 | ^3.10 | ^3.11" requests = "^2.20.0" importlib-resources = "^5.12.0" +docker = {version = "^6.1.0", optional = true} +testcontainers = {version = "^3.7.1", optional = true} [tool.poetry.group.dev.dependencies] black = "^23.3.0" @@ -45,6 +47,8 @@ watchdog = "^3.0.0" wheel = "^0.40.0" pytest = "^7.3.1" +[tool.poetry.extras] +testing = ["docker", "testcontainers"] [tool.pytest.ini_options] markers = [ diff --git a/tests/test_containers.py b/tests/test_containers.py new file mode 100644 index 0000000..cf3c193 --- /dev/null +++ b/tests/test_containers.py @@ -0,0 +1,402 @@ +from pathlib import Path +from typing import cast +from unittest.mock import MagicMock, Mock, patch + +import pytest +import requests +from docker.models.containers import Container + +from wiremock.client import ( + HttpMethods, + Mapping, + MappingRequest, + MappingResponse, + Mappings, +) +from wiremock.constants import Config +from wiremock.testing.testcontainer import ( + WireMockContainer, + WireMockContainerException, + wiremock_container, +) + + +@patch.object(WireMockContainer, "get_exposed_port") +@patch.object(WireMockContainer, "get_container_host_ip") +def test_get_secure_base_url(mock_get_ip, mock_get_port): + # Arrange + wm = WireMockContainer( + secure=True, + verify_ssl_certs=False, + ) + mock_get_ip.return_value = "127.0.0.1" + mock_get_port.return_value = 63379 + expected_url = "https://127.0.0.1:63379" + + # Act/Assert + assert wm.get_base_url() == expected_url + + +@patch.object(WireMockContainer, "get_exposed_port") +@patch.object(WireMockContainer, "get_container_host_ip") +def test_get_secure_url(mock_get_ip, mock_get_port): + # Arrange + wm = WireMockContainer( + secure=True, + verify_ssl_certs=False, + ) + path = "example-path" + mock_get_ip.return_value = "127.0.0.1" + mock_get_port.return_value = 63379 + + expected_url = "https://127.0.0.1:63379/example-path" + + # Act/Assert + assert wm.get_url(path) == expected_url + + +@patch.object(WireMockContainer, "get_exposed_port") +@patch.object(WireMockContainer, "get_container_host_ip") +def test_get_non_secure_base_url(mock_get_ip, mock_get_port): + # Arrange + wm = WireMockContainer( + secure=False, + verify_ssl_certs=False, + ) + mock_get_ip.return_value = "127.0.0.1" + mock_get_port.return_value = 63379 + expected_url = "http://127.0.0.1:63379" + + # Act/Assert + assert wm.get_base_url() == expected_url + + +@patch.object(WireMockContainer, "get_exposed_port") +@patch.object(WireMockContainer, "get_container_host_ip") +def test_get_non_secure_url(mock_get_ip, mock_get_port): + # Arrange + wm = WireMockContainer( + secure=False, + verify_ssl_certs=False, + ) + path = "example-path" + mock_get_ip.return_value = "127.0.0.1" + mock_get_port.return_value = 63379 + expected_url = "http://127.0.0.1:63379/example-path" + + # Act/Assert + assert wm.get_url(path) == expected_url + + +@patch.object(WireMockContainer, "initialize") +def test_initialize_method_call_on_instance_creation(mock_init): + + # Arrange/Act + WireMockContainer() + + # Assert + mock_init.assert_called_once_with() + + +@patch.object(WireMockContainer, "with_https_port") +def test_initialize_set_defaults(mock_set_secure_port): + + wm = WireMockContainer() + + assert wm.https_server_port == 8443 + assert wm.http_server_port == 8080 + assert wm.wire_mock_args == [] + assert wm.mapping_stubs == {} + assert wm.mapping_files == {} + assert wm.extensions == {} + mock_set_secure_port.assert_called_once_with() + + +@patch.object(WireMockContainer, "with_http_port") +@patch.object(WireMockContainer, "with_https_port") +def test_initialize_non_secure_mode_sets_http_port( + mock_set_secure_port, mock_set_unsecure_port +): + wm = WireMockContainer(secure=False) + + assert wm.https_server_port == 8443 + assert wm.http_server_port == 8080 + assert wm.wire_mock_args == [] + assert wm.mapping_stubs == {} + assert wm.mapping_files == {} + assert wm.extensions == {} + + assert not mock_set_secure_port.called + mock_set_unsecure_port.assert_called_once_with() + + +@patch.object(WireMockContainer, "with_exposed_ports") +@patch.object(WireMockContainer, "with_cli_arg") +def test_with_https_port_default(mock_cli_arg, mock_expose_port): + + # Arrange + wm = WireMockContainer(init=False) + + # Act + wm.with_https_port() + + # Assert + mock_cli_arg.assert_called_once_with("--https-port", "8443") + mock_expose_port.assert_called_once_with(wm.https_server_port) + + +@patch.object(WireMockContainer, "with_exposed_ports") +@patch.object(WireMockContainer, "with_cli_arg") +def test_with_https_port_with_user_defined_port_value(mock_cli_arg, mock_expose_port): + + # Arrange + wm = WireMockContainer(https_server_port=9443, init=False) + + # Act + wm.with_https_port() + + # Assert + mock_cli_arg.assert_called_once_with("--https-port", "9443") + mock_expose_port.assert_called_once_with(9443) + + +@patch.object(WireMockContainer, "with_exposed_ports") +@patch.object(WireMockContainer, "with_cli_arg") +def test_with_http_port_default(mock_cli_arg, mock_expose_port): + + # Arrange + wm = WireMockContainer(init=False) + + # Act + wm.with_http_port() + + # Assert + mock_cli_arg.assert_called_once_with("--port", "8080") + mock_expose_port.assert_called_once_with(wm.http_server_port) + + +@patch.object(WireMockContainer, "with_exposed_ports") +@patch.object(WireMockContainer, "with_cli_arg") +def test_with_http_port_with_user_defined_port_value(mock_cli_arg, mock_expose_port): + + # Arrange + wm = WireMockContainer(http_server_port=5000, init=False) + + # Act + wm.with_http_port() + + # Assert + mock_cli_arg.assert_called_once_with("--port", "5000") + mock_expose_port.assert_called_once_with(5000) + + +@patch("wiremock.testing.testcontainer.requests.get") +@patch.object(WireMockContainer, "get_url") +def test_container_starts_with_custom_https_port(mock_get_url, mock_get): + + # Arrange + mock_get_url.return_value = "http://localhost/__admin/mappings" + resp_mock = MagicMock(spec=requests.Response) + resp_mock.status_code = 200 + mock_get.return_value = resp_mock + wm = WireMockContainer(verify_ssl_certs=False, https_server_port=9443) + + # Act + assert wm.server_running() is True + + +@patch("wiremock.testing.testcontainer.requests.get") +@patch.object(WireMockContainer, "get_url") +def test_container_starts_with_custom_http_port(mock_get_url, mock_get): + + # Arrange + mock_get_url.return_value = "http://localhost/__admin/mappings" + resp_mock = MagicMock(spec=requests.Response) + resp_mock.status_code = 200 + mock_get.return_value = resp_mock + wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) + + # Act + + assert wm.server_running() is True + + +@patch("wiremock.testing.testcontainer.requests.get") +@patch.object(WireMockContainer, "get_url") +def test_container_not_running_returns_false(mock_get_url, mock_get): + + # Arrange + mock_get_url.return_value = "http://localhost/__admin/mappings" + resp_mock = MagicMock(spec=requests.Response) + resp_mock.status_code = 403 + mock_get.return_value = resp_mock + wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) + + # Act + + assert wm.server_running() is False + + +@patch("wiremock.testing.testcontainer.requests.post") +@patch.object(WireMockContainer, "get_url") +def test_reload_mappings(mock_get_url, mock_post): + + # Arrange + mock_get_url.return_value = "http://localhost/__admin/mappings" + resp_mock = MagicMock(spec=requests.Response) + resp_mock.status_code = 200 + mock_post.return_value = resp_mock + wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) + + # Act + resp = wm.reload_mappings() + + assert resp.status_code == 200 + + +@patch("wiremock.testing.testcontainer.requests.post") +@patch.object(WireMockContainer, "get_url") +def test_reload_mappings_failure_raises_exception(mock_get_url, mock_post): + + # Arrange + mock_get_url.return_value = "http://localhost/__admin/mappings" + resp_mock = MagicMock(spec=requests.Response) + resp_mock.status_code = 403 + mock_post.return_value = resp_mock + wm = WireMockContainer(verify_ssl_certs=False, secure=False, http_server_port=5000) + + # Act + with pytest.raises(WireMockContainerException): + wm.reload_mappings() + + +def test_container_with_cli_arg_sets_cmd_line_args(): + + # Arrange + wm = WireMockContainer() + + # Act + wm.with_cli_arg("--foo", "bar") + + # Assert + assert wm.wire_mock_args == ["--https-port", "8443", "--foo", "bar"] + + +def test_container_with_command_generates_command_from_cli_args(): + + # Arrange + wm = WireMockContainer() + + # Act + wm.with_command() + + # Assert + assert wm._command == "--https-port 8443" + + +def test_container_with_command_override(): + + # Arrange + wm = WireMockContainer() + + # Act + wm.with_command(cmd="--foo bar") + + # Assert + assert wm._command == "--foo bar" + + +@patch.object(WireMockContainer, "get_wrapped_container", spec=Container) +def test_copy_file_to_container(mock_get_container: Mock, tmp_path: Path): + + # Arrange + d = tmp_path / "mappings" + d.mkdir() + mapping = d / "mapping.json" + mapping.write_text('{"foo": "bar"}') + wm = WireMockContainer() + + # Act + wm.copy_file_to_container(mapping, Path(wm.MAPPINGS_DIR)) + + # Assert + mock_get_container.return_value.put_archive.assert_called_once_with( + path=Path(wm.MAPPINGS_DIR), data=b'{"foo": "bar"}' + ) + + +@pytest.mark.container_test +def test_configure_manually(): + + wm = cast( + WireMockContainer, + ( + WireMockContainer(verify_ssl_certs=False) + .with_mapping( + "hello-world.json", + { + "request": {"method": "GET", "url": "/hello"}, + "response": {"status": 200, "body": "hello"}, + }, + ) + .with_mapping( + "hello-world-file.json", + { + "request": {"method": "GET", "url": "/hello2"}, + "response": {"status": 200, "bodyFileName": "hello.json"}, + }, + ) + .with_file("hello.json", {"message": "Hello World !"}) + .with_cli_arg("--verbose", "") + .with_cli_arg("--root-dir", "/home/wiremock") + .with_env("JAVA_OPTS", "-Djava.net.preferIPv4Stack=true") + ), + ) + with wm: + resp1 = requests.get(wm.get_url("/hello"), verify=False) + resp2 = requests.get(wm.get_url("/hello2"), verify=False) + assert resp1.status_code == 200 + assert resp1.content == b"hello" + assert resp2.status_code == 200 + assert resp2.content == b'{"message": "Hello World !"}' + + +@pytest.mark.container_test +def test_configure_via_wiremock_container_context_manager(): + + mappings = [ + ( + "hello-world.json", + { + "request": {"method": "GET", "url": "/hello"}, + "response": {"status": 200, "body": "hello"}, + }, + ) + ] + + with wiremock_container(mappings=mappings, verify_ssl_certs=False) as wm: + + resp1 = requests.get(wm.get_url("/hello"), verify=False) + assert resp1.status_code == 200 + assert resp1.content == b"hello" + + +@pytest.mark.container_test +def test_container_sdk_integration(): + + with wiremock_container(secure=False) as wm: + + Config.base_url = wm.get_url("__admin") + + Mappings.create_mapping( + Mapping( + priority=100, + request=MappingRequest(method=HttpMethods.GET, url="/hello"), + response=MappingResponse(status=200, body="hello"), + persistent=False, + ) + ) + + resp1 = requests.get(wm.get_url("/hello"), verify=False) + assert resp1.status_code == 200 + assert resp1.content == b"hello" diff --git a/wiremock/base/base_resource.py b/wiremock/base/base_resource.py index 17b2e8e..714da80 100644 --- a/wiremock/base/base_resource.py +++ b/wiremock/base/base_resource.py @@ -1,4 +1,6 @@ import json +from typing import List +from urllib.parse import urljoin import requests from requests import exceptions as rexc @@ -39,6 +41,11 @@ def _log(self, action, url, **kwargs): extra=ctx, ) + def _get_url(self, *uri_parts: List[str]) -> str: + + uri = "/".join(map(lambda x: str(x).rstrip("/"), uri_parts)) + return f"{self._base_url().rstrip('/')}{uri}" + def post(self, uri, **kwargs): if "timeout" not in kwargs: kwargs["timeout"] = self._timeout() @@ -47,7 +54,7 @@ def post(self, uri, **kwargs): if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("POST", url, **kwargs) return requests.post(url, **kwargs) except rexc.Timeout as e: # pragma: no cover @@ -63,7 +70,7 @@ def get(self, uri, **kwargs): if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("GET", url, **kwargs) return requests.get(url, **kwargs) except rexc.Timeout as e: # pragma: no cover @@ -79,7 +86,7 @@ def put(self, uri, **kwargs): if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("PUT", url, **kwargs) return requests.put(url, **kwargs) except rexc.Timeout as e: # pragma: no cover @@ -95,7 +102,7 @@ def patch(self, uri, **kwargs): # pragma: no cover if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("PATCH", url, **kwargs) return requests.patch(url, **kwargs) except rexc.Timeout as e: # pragma: no cover @@ -111,7 +118,7 @@ def delete(self, uri, **kwargs): if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("DELETE", url, **kwargs) return requests.delete(url, **kwargs) except rexc.Timeout as e: # pragma: no cover @@ -127,7 +134,7 @@ def options(self, uri, **kwargs): # pragma: no cover if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("OPTIONS", url, **kwargs) return requests.options(url, **kwargs) except rexc.Timeout as e: # pragma: no cover @@ -143,7 +150,7 @@ def head(self, uri, **kwargs): # pragma: no cover if "requests_cert" not in kwargs: kwargs["cert"] = self._requests_cert() try: - url = self._base_url() + uri + url = self._get_url(uri) self._log("HEAD", url, **kwargs) return requests.head(url, **kwargs) except rexc.Timeout as e: # pragma: no cover diff --git a/wiremock/testing/__init__.py b/wiremock/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wiremock/testing/testcontainer.py b/wiremock/testing/testcontainer.py new file mode 100644 index 0000000..3519250 --- /dev/null +++ b/wiremock/testing/testcontainer.py @@ -0,0 +1,307 @@ +import json +import os +import tarfile +import tempfile +import time +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Dict, Generator, List, Optional, Tuple, Union +from urllib.parse import urljoin + +import docker +import requests +from testcontainers.core.container import DockerContainer +from testcontainers.core.exceptions import ContainerStartException +from testcontainers.core.waiting_utils import wait_container_is_ready + +from wiremock.resources.mappings.models import Mapping + +TMappingConfigs = Dict[Union[str, Dict], Union[str, int, Dict, Mapping]] + + +class WireMockContainerException(Exception): + pass + + +class WireMockContainer(DockerContainer): + """ + Wiremock container. + """ + + MAPPINGS_DIR: str = "/home/wiremock/mappings/" + FILES_DIR: str = "/home/wiremock/__files/" + + def __init__( + self, + image: str = "wiremock/wiremock:2.35.0", + http_server_port: int = 8080, + https_server_port: int = 8443, + secure: bool = True, + verify_ssl_certs: bool = True, + init: bool = True, + docker_client_kwargs: Dict[str, Any] = {}, + ) -> None: + self.http_server_port = http_server_port + self.https_server_port = https_server_port + self.secure = secure + self.verify_ssl_certs = verify_ssl_certs + super(WireMockContainer, self).__init__(image, **docker_client_kwargs) + + if init: + self.initialize() + + def initialize(self) -> None: + self.wire_mock_args: List[str] = [] + self.mapping_stubs: Dict[str, str] = {} + self.mapping_files: Dict[str, str] = {} + self.extensions: Dict[str, bytes] = {} + + if self.secure: + self.with_https_port() + else: + self.with_http_port() + + def with_http_port(self) -> None: + self.with_cli_arg("--port", str(self.http_server_port)) + self.with_exposed_ports(self.http_server_port) + + def with_https_port(self) -> None: + self.with_cli_arg("--https-port", str(self.https_server_port)) + self.with_exposed_ports(self.https_server_port) + + def with_cli_arg(self, arg_name: str, arg_value: str) -> "WireMockContainer": + self.wire_mock_args.append(arg_name) + self.wire_mock_args.append(arg_value) + return self + + def with_mapping(self, name: str, data: TMappingConfigs) -> "WireMockContainer": + self.mapping_stubs[name] = json.dumps(data) + return self + + def with_file(self, name: str, data: Dict[str, Any]): + self.mapping_files[name] = json.dumps(data) + return self + + def with_command(self, cmd: Optional[str] = None) -> "WireMockContainer": + + if not cmd: + cmd = " ".join(self.wire_mock_args) + + super().with_command(cmd) + + return self + + def copy_file_to_container(self, host_path: Path, container_path: Path) -> None: + with open(host_path, "rb") as fp: + self.get_wrapped_container().put_archive( + path=container_path, data=fp.read() + ) + + def copy_files_to_container( + self, configs: Dict[str, Any], container_dir_path: Path, mode: str = "w+" + ) -> None: + + temp_dir = tempfile.mkdtemp() + + # generate temp files all config files + for config_name, config_content in configs.items(): + file_name = os.path.basename(config_name) + destination_path = os.path.join(temp_dir, file_name) + with open(destination_path, mode) as fp: + fp.write(config_content) + + # tar all files from temp dir + tarfile_path = f"{temp_dir}.tar.gz" + with tarfile.open(tarfile_path, "w:gz") as tar: + for root, _, files in os.walk(temp_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, temp_dir) + tar.add(file_path, arcname=arcname) + + # copy tar archive onto container and extract at {container_dir_path} + self.copy_file_to_container( + host_path=Path(tarfile_path), container_path=container_dir_path + ) + + def copy_mappings_to_container(self) -> None: + """Copies all mappings files generated with + `.with_mapping('hello-world.json', {...})` to the container under + the configured MAPPINGS_DIR + """ + + self.copy_files_to_container( + configs=self.mapping_stubs, container_dir_path=Path(f"{self.MAPPINGS_DIR}") + ) + + def copy_mapping_files_to_container(self) -> None: + """Copies all mappings files generated with + `.with_file('hello.json', {...})` to the container under + the configured FILES_DIR + """ + self.copy_files_to_container( + configs=self.mapping_files, container_dir_path=Path(f"{self.FILES_DIR}") + ) + + def server_running(self, retry_count: int = 3, retry_delay: int = 1) -> bool: + """Pings the __admin/mappings endpoint of the wiremock server running inside the + container as a proxy for checking if the server is up and running. + + {retry_count} attempts requests will be made with a delay of {retry_delay} + to allow for race conditions when containers are being spun up + quickly between tests. + + Args: + retry_count: The number of attempts made to ping the server + retry_delay: The number of seconds to wait before each attempt + + Returns: + True if the request is successful + """ + + for _ in range(retry_count): + try: + response = requests.get( + self.get_url("__admin/mappings"), verify=self.verify_ssl_certs + ) + if response.status_code == 200: + return True + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + + time.sleep(retry_delay) + + return False + + def reload_mappings(self) -> requests.Response: + """When mappings are mounted into a container via files + the server will already be running as it will start as soon as the container + starts. reload_mappings is called via the rest api to ensure any mappings + added after the server starts are picked up. + """ + resp = requests.post( + self.get_url("__admin/mappings/reset"), verify=self.verify_ssl_certs + ) + if not resp.status_code <= 300: + raise WireMockContainerException("Failed to reload mappings") + + return resp + + @wait_container_is_ready() + def configure(self) -> None: + if not self.server_running(): + raise WireMockContainerException( + "Server does not appear to be running in container" + ) + + self.copy_mappings_to_container() + self.copy_mapping_files_to_container() + + self.reload_mappings() + + def get_base_url(self) -> str: + """Generate the base url of the container wiremock-server + + Returns: + The base to the container based on the hostname and exposed ports + """ + proto = "https" if self.secure else "http" + port = self.https_server_port if self.secure else self.http_server_port + + if os.environ.get("WIREMOCK_DIND", False): + host = "host.docker.internal" + else: + host = self.get_container_host_ip() + + return f"{proto}://{host}:{self.get_exposed_port(port)}" + + def get_url(self, path: str) -> str: + return urljoin(self.get_base_url(), path) + + def start(self, cmd: Optional[str] = None) -> "WireMockContainer": + self.with_command(cmd) + super().start() + self.configure() + return self + + +@contextmanager +def wiremock_container( + image: str = "wiremock/wiremock:2.35.0", + http_server_port: int = 8080, + https_server_port: int = 8443, + secure: bool = True, + verify_ssl_certs: bool = True, + mappings: List[Tuple[str, TMappingConfigs]] = [], + start: bool = True, + docker_client_kwargs: Dict[str, Any] = {}, +) -> Generator[WireMockContainer, None, None]: + """ + Start a wiremock test container using Testcontainers + + Attributes + image (str): specify the docker image name and version for wiremock server. + http_server_port (int): The port of the HTTP server port + https_server_port (int): The port of the HTTPS server port + secure (bool): Set True If you're connecting to the server via ssl. + verify_ssl_certs (bool): Should requests verify ssl certs when using + secure connections. + mappings list[Tuple[str, TMappingConfigs]]: a list of tuples containing + mapping name and mapping dictionary. + start (bool): If true, start the container, otherwise just yield + container instance + docker_client_kwargs (dict): Kwargs to pass to the docker client + + Examples: + + Mappings can be provided as tuples of mapping name, TMappingConfigs. This + will create mapping config files in the container. + + ``` + mappings = [ + ( + "hello-world.json", + { + "request": {"method": "GET", "url": "/hello"}, + "response": {"status": 200, "body": "hello"}, + }, + ) + ] + + with wiremock_container(mappings=mappings, verify_ssl_certs=False) as wm: + + resp1 = requests.get(wm.get_url("/hello"), verify=False) + assert resp1.status_code == 200 + ``` + + Or you can use the SDK directly to create mappings via the API. + + + :return: WireMockContainer instance + """ + + client = docker.from_env() + client.ping() + try: + wm = WireMockContainer( + image=image, + http_server_port=http_server_port, + https_server_port=https_server_port, + secure=secure, + verify_ssl_certs=verify_ssl_certs, + docker_client_kwargs=docker_client_kwargs, + ) + [wm.with_mapping(m_name, m_data) for m_name, m_data in mappings] + if start: + with wm: + yield wm + else: + yield wm + except ContainerStartException as e: + raise WireMockContainerException("Error starting wiremock container") from e + except requests.exceptions.RequestException as e: + raise WireMockContainerException( + "Error connecting to wiremock container" + ) from e + finally: + client.close()