pax_global_header00006660000000000000000000000064147221406000014506gustar00rootroot0000000000000052 comment=44778067b07e97bfe29bcdd1ba2dc9511d20bb67 pytest_httpx-0.35.0/000077500000000000000000000000001472214060000143525ustar00rootroot00000000000000pytest_httpx-0.35.0/.github/000077500000000000000000000000001472214060000157125ustar00rootroot00000000000000pytest_httpx-0.35.0/.github/workflows/000077500000000000000000000000001472214060000177475ustar00rootroot00000000000000pytest_httpx-0.35.0/.github/workflows/release.yml000066400000000000000000000010411472214060000221060ustar00rootroot00000000000000name: Release on: push: branches: - master jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.13' - name: Create packages run: | python -m pip install build python -m build . - name: Publish packages run: | python -m pip install twine python -m twine upload dist/* --skip-existing --username __token__ --password ${{ secrets.pypi_password }} pytest_httpx-0.35.0/.github/workflows/test.yml000066400000000000000000000013571472214060000214570ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -e .[testing] - name: Test run: | pytest --cov=pytest_httpx --cov-fail-under=100 --cov-report=term-missing --runpytest=subprocess - name: Test packages creation run: | python -m pip install build python -m build . pytest_httpx-0.35.0/.gitignore000066400000000000000000000025371472214060000163510ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### VirtualEnv template # Virtualenv # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ .Python [Bb]in [Ii]nclude [Ll]ib [Ll]ib64 [Ll]ocal [Ss]cripts pyvenv.cfg .venv pip-selfcheck.json ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject .idea/ .pypirc pip.ini pytest_httpx-0.35.0/.pre-commit-config.yaml000066400000000000000000000001331472214060000206300ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: blackpytest_httpx-0.35.0/CHANGELOG.md000066400000000000000000000571541472214060000161770ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.35.0] - 2024-11-28 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.28.\* ## [0.34.0] - 2024-11-18 ### Added - `is_optional` parameter is now available on responses and callbacks registration. Allowing to add optional responses while keeping other responses as mandatory. Refer to documentation for more details. - `is_reusable` parameter is now available on responses and callbacks registration. Allowing to add multi-match responses while keeping other responses as single-match. Refer to documentation for more details. ### Fixed - `httpx_mock.get_request` will now also propose to refine filters if more than one request is found instead of only proposing to switch to `httpx_mock.get_requests`. ## [0.33.0] - 2024-10-28 ### Added - Explicit support for python `3.13`. - `should_mock` option (callable returning a boolean) is now available, defaulting to always returning `True`. Refer to documentation for more details. - Matching on the full multipart body can now be performed using `match_files` and `match_data` parameters. Refer to documentation for more details. - Matching on extensions (including timeout) can now be performed using `match_extensions` parameter. Refer to documentation for more details. ### Removed - `non_mocked_hosts` option is not available anymore. Use `should_mock` instead as in the following sample: ```python import pytest @pytest.mark.httpx_mock(non_mocked_hosts=["my_local_test_host"]) def test_previous_behavior(httpx_mock): ... @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host not in ["my_local_test_host"]) def test_new_behavior(httpx_mock): ... ``` Please note that your hosts might need to be prefixed with `www.` depending on your usage. ## [0.32.0] - 2024-09-27 ### Added - The following option is now available: - `can_send_already_matched_responses` (boolean), defaulting to `False`. - Assertion failure message in case of unmatched responses is now linking documentation on how to deactivate the check. - Assertion failure message in case of unmatched requests is now linking documentation on how to deactivate the check. - `httpx.TimeoutException` message issued in case of unmatched request is now linking documentation on how to reuse responses (in case some responses are already matched). ### Fixed - Documentation now clearly state the risks associated with changing the default options. - Assertion failure message in case of unmatched requests at teardown is now describing requests in a more user-friendly way. - Assertion failure message in case of unmatched requests at teardown is now prefixing requests with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists. - Assertion failure message in case of unmatched responses at teardown is now prefixing responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists. - `httpx.TimeoutException` message issued in case of unmatched request is now prefixing available responses with `- ` to highlight the fact that this is a list, preventing misapprehension in case only one element exists. - `httpx.TimeoutException` message issued in case of unmatched request is now listing unmatched responses (in registration order) before already matched one (still in registration order). - The incentive behind this change is to help identify a potential mismatch faster as the first unmatched response is the most likely to be the one expected to match. - Response description in failure messages (`httpx.TimeoutException` message issued in case of unmatched request or assertion failure message in case of unmatched responses at teardown) is now displaying if the response was already matched or not and less misleading in it's phrasing about what it can match (a single request by default). ### Changed - Last registered matching response will not be reused by default anymore in case all matching responses have already been sent. - This behavior can be changed thanks to the new `pytest.mark.httpx_mock(can_send_already_matched_responses=True)` option. - The incentive behind this change is to spot regression if a request was issued more than the expected number of times. - `HTTPXMock` class was only exposed for type hinting purpose. This is now explained in the class docstring. - As a result this is the last time a change to `__init__` signature will be documented and considered a breaking change. - Future changes will not be documented and will be considered as internal refactoring not worth a version bump. - `__init__` now expects one parameter, the newly introduced (since [0.31.0]) options. - `HTTPXMockOptions` class was never intended to be exposed and is now marked as private. ## [0.31.2] - 2024-09-23 ### Fixed - `httpx_mock` marker can now be defined at different levels for a single test. ## [0.31.1] - 2024-09-22 ### Fixed - It is now possible to match on content provided as async iterable by the client. ## [0.31.0] - 2024-09-20 ### Changed - Tests will now fail at teardown by default if some requests were issued but were not matched. - This behavior can be changed thanks to the new `pytest.mark.httpx_mock(assert_all_requests_were_expected=False)` option. - The incentive behind this change is to spot unexpected requests in case code is swallowing `httpx.TimeoutException`. - The `httpx_mock` fixture is now configured using a marker (many thanks to [`Frazer McLean`](https://github.com/RazerM)). ```python # Apply marker to whole module pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False) # Or to specific tests @pytest.mark.httpx_mock(non_mocked_hosts=[...]) def test_foo(httpx_mock): ... ``` - The following options are available: - `assert_all_responses_were_requested` (boolean), defaulting to `True`. - `assert_all_requests_were_expected` (boolean), defaulting to `True`. - `non_mocked_hosts` (iterable), defaulting to an empty list, meaning all hosts are mocked. - `httpx_mock.reset` do not expect any parameter anymore and will only reset the mock state (no assertions will be performed). ### Removed - `pytest` `7` is not supported anymore (`pytest` `8` has been out for 9 months already). - `assert_all_responses_were_requested` fixture is not available anymore, use `pytest.mark.httpx_mock(assert_all_responses_were_requested=False)` instead. - `non_mocked_hosts` fixture is not available anymore, use `pytest.mark.httpx_mock(non_mocked_hosts=[])` instead. ## [0.30.0] - 2024-02-21 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.27.\* ### Fixed - Switch from `setup.py` to `pyproject.toml` (many thanks to [`Felix Scherz`](https://github.com/felixscherz)). ## [0.29.0] - 2024-01-29 ### Added - Add support for [`pytest`](https://docs.pytest.org)==8.\* ([`pytest`](https://docs.pytest.org)==7.\* is still supported for now) (many thanks to [`Yossi Rozantsev`](https://github.com/Apakottur)). ## [0.28.0] - 2023-12-21 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.26.\* ## [0.27.0] - 2023-11-13 ### Added - Explicit support for python `3.12`. ### Fixed - Custom HTTP transport are now handled (parent call to `handle_async_request` or `handle_request`). ### Changed - Only HTTP transport are now mocked, this should not have any impact, however if it does, please feel free to open an issue describing your use case. ## [0.26.0] - 2023-09-18 ### Added - Added `proxy_url` parameter which allows matching on proxy URL. ## [0.25.0] - 2023-09-11 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.25.\* ### Removed - `pytest` `6` is no longer supported. ## [0.24.0] - 2023-09-04 ### Added - Added `match_json` parameter which allows matching on JSON decoded body (matching against python representation instead of bytes). ### Changed - Even if it was never documented as a feature, the `match_headers` parameter was not considering header names case when matching. - As this might have been considered a feature by some users, the fact that `match_headers` will now respect casing is documented as a breaking change. ### Fixed - Matching on headers does not ignore name case anymore, the name must now be cased as sent (as some servers might expect a specific case). - Error message in case a request does not match will now include request headers with mismatching name case as well. - Error message in case a request does not match will now include request headers when not provided as lower-cased to `match_headers`. - Add `:Any` type hint to `**matchers` function arguments to satisfy strict type checking mode in [`pyright`](https://microsoft.github.io/pyright/#/). ## [0.23.1] - 2023-08-02 ### Fixed - Version `0.23.0` introduced a regression removing the support for mutating json content provided in `httpx_mock.add_response`. - This is fixed, you can now expect the JSON return being as it was when provided to `httpx_mock.add_response`: ```python mutating_json = {"content": "request 1"} # This will return {"content": "request 1"} httpx_mock.add_response(json=mutating_json) mutating_json["content"] = "request 2" # This will return {"content": "request 2"} httpx_mock.add_response(json=mutating_json) ``` ## [0.23.0] - 2023-08-02 ### Removed - Python `3.7` and `3.8` are no longer supported. ### Fixed - `httpx_mock.add_response` is now returning a new `httpx.Response` instance upon each matching request. Preventing unnecessary recursion in streams. ## [0.22.0] - 2023-04-12 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.24.\* ## [0.21.3] - 2023-01-20 ### Fixed - Update version specifiers for `pytest` dependency to support `packaging` `23`. - Add explicit support for `python` `3.11`. ## [0.21.2] - 2022-11-03 ### Fixed - URL containing non ASCII characters in query can now be matched. - Requests are now cleared when calling `httpx_mock.reset`. ## [0.21.1] - 2022-10-20 ### Fixed - `httpx_mock.add_callback` now handles async callbacks. ## [0.21.0] - 2022-05-24 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.23.\* ### Removed - Python `3.6` is no longer supported. ## [0.20.0] - 2022-02-05 ### Added - Add support for [`pytest`](https://docs.pytest.org)==7.\* ([`pytest`](https://docs.pytest.org)==6.\* is still supported for now) (many thanks to [`Craig Blaszczyk`](https://github.com/jakul)). ## [0.19.0] - 2022-01-26 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.22.\* ### Deprecated - Python 3.6 is no longer supported. ## [0.18.0] - 2022-01-17 ### Fixed - Callback are now executed as expected when there is a matching already sent response. ### Changed - Registration order is now looking at responses and callbacks. Prior to this version, registration order was looking at responses before callbacks. ### Removed - `httpx_mock.add_response` `data`, `files` and `boundary` parameters have been removed. It was deprecated since `0.17.0`. Refer to this version changelog entry for more details on how to update your code. ## [0.17.3] - 2021-12-27 ### Fixed - A callback can now raise an exception again (regression in mypy check since [0.16.0]). ### Added - An exception can now be raised without creating a callback by using `httpx_mock.add_exception` method. ## [0.17.2] - 2021-12-23 ### Fixed - Do not consider a callback response as read, even if it is not a stream, before returning to `httpx`. Allowing any specific httpx handling to be triggered such as `httpx.Response.elapsed` computing. ## [0.17.1] - 2021-12-20 ### Fixed - Do not consider a response as read, even if it is not a stream, before returning to `httpx`. Allowing any specific httpx handling to be triggered such as `httpx.Response.elapsed` computing. ## [0.17.0] - 2021-12-20 ### Changed - `httpx_mock.add_response` `data` parameter is only used for multipart content. It was deprecated since `0.14.0`. Refer to this version changelog entry for more details on how to update your code. ### Removed - `pytest_httpx.to_response` function has been removed. It was deprecated since `0.14.0`. Refer to this version changelog entry for more details on how to update your code. ### Deprecated - `httpx_mock.add_response` `data`, `files` and `boundary` parameters that were only used for multipart content. Instead, provide the `stream` parameter with an instance of the `httpx._multipart.MultipartStream`. ### Fixed - Responses are no more read or closed when returned to the client. Allowing to add a response once and reading it as a new response on every request. ## [0.16.0] - 2021-12-20 ### Changed - Callbacks are now expected to have a single parameter, the request. The previously second parameter `extensions`, can still be accessed via `request.extensions`. ### Fixed - Allow for users to run `mypy --strict`. ## [0.15.0] - 2021-11-16 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.21.\* ## [0.14.0] - 2021-10-22 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.20.\* (many thanks to [`Terence Honles`](https://github.com/terencehonles)) - Callbacks are now expected to return a `httpx.Response` instance instead of the previous `httpcore.Response` tuple. As a consequence, `pytest_httpx.to_response` now returns a `httpx.Response` instance. ### Added - `httpx_mock.add_response` now allows to explicitly provide bytes using `content` parameter. - `httpx_mock.add_response` now allows to explicitly provide string using `text` parameter. - `httpx_mock.add_response` now allows to explicitly provide HTML string content using `html` parameter. - `httpx_mock.add_response` now allows to explicitly provide streamed content using `stream` parameter and the new `pytest_httpx.IteratorStream` class. ### Deprecated - `pytest_httpx.to_response` is now deprecated in favor of `httpx.Response`. This function will be removed in a future release. - `httpx_mock.add_response` `data` parameter should now only be used for multipart content. Instead, use the appropriate parameter amongst `content`, `text`, `html` or `stream`. ## [0.13.0] - 2021-08-19 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.19.\* - `files` parameter of `httpx_mock.add_response` now expect dictionary values to be binary (as per [`httpx` new requirement](https://github.com/encode/httpx/blob/master/CHANGELOG.md#0190-19th-june-2021)). ## [0.12.1] - 2021-08-11 ### Fixed - Type information is now provided following [PEP 561](https://www.python.org/dev/peps/pep-0561/) (many thanks to [`Caleb Ho`](https://github.com/calebho)). ## [0.12.0] - 2021-04-27 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.18.\* - `ext` callback parameter was renamed into `extensions`. ## [0.11.0] - 2021-03-01 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.17.\* ## [0.10.1] - 2020-11-25 ### Fixed - Order of different parameters does not matter anymore for URL matching. It does however still matter for a same parameter. ## [0.10.0] - 2020-10-06 ### Added - Document how to assert that no requests were issued. - Document how to send cookies. - Explicit support for python 3.9 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.16.\* - Update documentation to reflect the latest way of sending bytes using `httpx`. Via `content` parameter instead of `data`. - Code now follow `black==20.8b1` formatting instead of the git master version. - Sending a JSON response using `json` parameter will now set the `application/json` content-type header by default. ### Fixed - Allow to provide any supported `httpx` headers type in headers parameter for `httpx_mock.add_response` and `pytest_httpx.to_response`. Previously only dict was supported. ## [0.9.0] - 2020-09-22 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.15.\* - Callbacks are now called with `ext` dictionary instead of `timeout`. To follow `httpcore` design changes. You can still retrieve timeout by using ```ext['timeout']``` ## [0.8.0] - 2020-08-26 ### Added - `non_mocked_hosts` fixture allowing to avoid mocking requests sent on some specific hosts. ### Changed - Display the matchers that were not matched instead of the responses that were not sent. ## [0.7.0] - 2020-08-13 ### Changed - The `httpx.HTTPError` message issued in case no mock could be found is now a `httpx.TimeoutException` containing all information required to fix the test case (if needed). ## [0.6.0] - 2020-08-07 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.14.\* ## [0.5.0] - 2020-07-31 ### Changed - requires [`pytest`](https://docs.pytest.org/en/latest/) 6. - `assert_and_reset` mock method has been renamed to `reset` and now takes a boolean parameter to specify if assertion should be performed. ### Added - It is now possible to disable the assertion that all registered responses were requested thanks to the `assert_all_responses_were_requested` fixture. Refer to documentation for more details. ### Removed - It is not possible to provide an URL encoded response anymore by providing a dictionary in `data` parameter. ## [0.4.0] - 2020-06-05 ### Changed - `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture does not need to be explicitly imported anymore (many thanks to [`Thomas LÉVEIL`](https://github.com/thomasleveil)). ## [0.3.0] - 2020-05-24 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.13.\* - requires [`pytest`](https://docs.pytest.org/en/latest/) 5.4.0 (at least) - callbacks must now return a tuple as per `httpcore` specifications. Refer to documentation for more details. - callbacks timeout parameter is now a dict as per `httpcore` specifications. ## [0.2.1] - 2020-03-20 ### Fixed - Handle the fact that some classes and functions we use are now part of internals within [`httpx`](https://www.python-httpx.org). ## [0.2.0] - 2020-03-09 ### Changed - Requires [`httpx`](https://www.python-httpx.org)==0.12.\* ## [0.1.0] - 2020-02-13 ### Added - Consider as stable. ## [0.0.5] - 2020-02-10 ### Added - match_headers parameter now allows matching on headers. - match_content parameter now allows matching on full body. ### Changed - `httpx.HTTPError` is now raised instead of `Exception` in case a request cannot be matched. ## [0.0.4] - 2020-02-07 ### Changed - url is not a mandatory parameter for response registration anymore. - url is not a mandatory parameter for callback registration anymore. - url is not a mandatory parameter for request retrieval anymore. - method does not have a default value for response registration anymore. - method does not have a default value for callback registration anymore. - method does not have a default value for request retrieval anymore. - url and methods are not positional arguments anymore. ## [0.0.3] - 2020-02-06 ### Added - Allow providing JSON response as python values. - Mock async `httpx` requests as well. - Allow providing files and boundary for multipart response. - Allow to provide data as a dictionary for multipart response. - Allow providing callbacks that are executed upon reception of a request. - Handle the fact that parameters may be introduced in `httpx` *Dispatcher.send method. - Allow to retrieve all matching requests with HTTPXMock.get_requests ### Changed - method can now be provided even if not entirely upper-cased. - content parameter renamed into data. - HTTPXMock.get_request now fails if more than one request match. Use HTTPXMock.get_request instead. - HTTPXMock.requests is now private, use HTTPXMock.get_requests instead. - HTTPXMock.responses is now private, it should not be accessed anyway. - url can now be a re.Pattern instance. ## [0.0.2] - 2020-02-06 ### Added - Allow to retrieve requests. ## [0.0.1] - 2020-02-05 ### Added - First release, should be considered as unstable for now as design might change. [Unreleased]: https://github.com/Colin-b/pytest_httpx/compare/v0.35.0...HEAD [0.35.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.34.0...v0.35.0 [0.34.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.33.0...v0.34.0 [0.33.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.32.0...v0.33.0 [0.32.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.2...v0.32.0 [0.31.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.1...v0.31.2 [0.31.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.31.0...v0.31.1 [0.31.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.30.0...v0.31.0 [0.30.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.29.0...v0.30.0 [0.29.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.28.0...v0.29.0 [0.28.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.27.0...v0.28.0 [0.27.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.26.0...v0.27.0 [0.26.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.25.0...v0.26.0 [0.25.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.24.0...v0.25.0 [0.24.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.1...v0.24.0 [0.23.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.23.0...v0.23.1 [0.23.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.22.0...v0.23.0 [0.22.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.3...v0.22.0 [0.21.3]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.2...v0.21.3 [0.21.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.20.0...v0.21.0 [0.20.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.19.0...v0.20.0 [0.19.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.18.0...v0.19.0 [0.18.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.3...v0.18.0 [0.17.3]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.2...v0.17.3 [0.17.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.1...v0.17.2 [0.17.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.17.0...v0.17.1 [0.17.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.16.0...v0.17.0 [0.16.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.15.0...v0.16.0 [0.15.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.14.0...v0.15.0 [0.14.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.12.1...v0.13.0 [0.12.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.12.0...v0.12.1 [0.12.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.11.0...v0.12.0 [0.11.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.10.1...v0.11.0 [0.10.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.10.0...v0.10.1 [0.10.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.8.0...v0.9.0 [0.8.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.2.1...v0.3.0 [0.2.1]: https://github.com/Colin-b/pytest_httpx/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.5...v0.1.0 [0.0.5]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.4...v0.0.5 [0.0.4]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.3...v0.0.4 [0.0.3]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.2...v0.0.3 [0.0.2]: https://github.com/Colin-b/pytest_httpx/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/Colin-b/pytest_httpx/releases/tag/v0.0.1 pytest_httpx-0.35.0/CONTRIBUTING.md000066400000000000000000000041701472214060000166050ustar00rootroot00000000000000# How to contribute Everyone is free to contribute on this project. There are two ways to contribute: - Submit an issue. - Submit a pull request. ## Submitting an issue Before creating an issue please make sure that it was not already reported. ### When? - You encountered an issue. - You have a change proposal. - You have a feature request. ### How? 1) Go to the *Issues* tab and click on the *New issue* button. 2) Title should be a small sentence describing the request. 3) The comment should contain as much information as possible * Actual behavior (including the version you used) * Expected behavior * Steps to reproduce ## Submitting a pull request ### When? - You fixed an issue. - You changed something. - You added a new feature. ### How? #### Code 1) Create a new branch based on `develop` branch. 2) Fetch all dev dependencies. * Install required python modules using `pip`: **python -m pip install .[testing]** 3) Ensure tests are ok by running them using [`pytest`](https://doc.pytest.org/en/latest/index.html). 4) Add your changes. 5) Follow [Black](https://black.readthedocs.io/en/stable/) code formatting. * Install [pre-commit](https://pre-commit.com) python module using `pip`: **python -m pip install pre-commit** * To add the [pre-commit](https://pre-commit.com) hook, after the installation run: **pre-commit install** 6) Add at least one [`pytest`](https://doc.pytest.org/en/latest/index.html) test case. * Unless it is an internal refactoring request or a documentation update. 7) Add related [changelog entry](https://keepachangelog.com/en/1.1.0/) in the `Unreleased` section. * Unless it is a documentation update. #### Enter pull request 1) Go to the *Pull requests* tab and click on the *New pull request* button. 2) *base* should always be set to `develop` and it should be compared to your branch. 3) Title should be a small sentence describing the request. 4) The comment should contain as much information as possible * Actual behavior (before the new code) * Expected behavior (with the new code) * Steps to reproduce (with and without the new code to see the difference) pytest_httpx-0.35.0/LICENSE000066400000000000000000000020571472214060000153630ustar00rootroot00000000000000MIT License Copyright (c) 2024 Colin Bounouar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest_httpx-0.35.0/MANIFEST.in000066400000000000000000000000621472214060000161060ustar00rootroot00000000000000include CHANGELOG.md recursive-include tests *.py pytest_httpx-0.35.0/README.md000066400000000000000000000767721472214060000156540ustar00rootroot00000000000000

Send responses to HTTPX using pytest

pypi version Build status Coverage Code style: black Number of tests Number of downloads

> [!NOTE] > Version 1.0.0 will be released once httpx is considered as stable (release of 1.0.0). > > However, current state can be considered as stable. Once installed, `httpx_mock` [`pytest`](https://docs.pytest.org/en/latest/) fixture will make sure every [`httpx`](https://www.python-httpx.org) request will be replied to with user provided responses ([unless some hosts are explicitly skipped](#do-not-mock-some-requests)). - [Add responses](#add-responses) - [JSON body](#add-json-response) - [Custom body](#reply-with-custom-body) - [Multipart body (files, ...)](#add-multipart-response) - [HTTP status code](#add-non-200-response) - [HTTP headers](#reply-with-custom-headers) - [HTTP/2.0](#add-http/2.0-response) - [Add dynamic responses](#dynamic-responses) - [Raising exceptions](#raising-exceptions) - [Check requests](#check-sent-requests) - [Configuration](#configuring-httpx_mock) - [Register more responses than requested](#allow-to-register-more-responses-than-what-will-be-requested) - [Register less responses than requested](#allow-to-not-register-responses-for-every-request) - [Allow to register a response for more than one request](#allow-to-register-a-response-for-more-than-one-request) - [Do not mock some requests](#do-not-mock-some-requests) - [Migrating](#migrating-to-pytest-httpx) - [responses](#from-responses) - [aioresponses](#from-aioresponses) ## Add responses You can register responses for both sync and async [`HTTPX`](https://www.python-httpx.org) requests. ```python import pytest import httpx def test_something(httpx_mock): httpx_mock.add_response() with httpx.Client() as client: response = client.get("https://test_url") @pytest.mark.asyncio async def test_something_async(httpx_mock): httpx_mock.add_response() async with httpx.AsyncClient() as client: response = await client.get("https://test_url") ``` If all registered responses are not sent back during test execution, the test case will fail at teardown [(unless you turned `assert_all_responses_were_requested` option off)](#allow-to-register-more-responses-than-what-will-be-requested). Default response is a `HTTP/1.1` `200 (OK)` without any body. ### How response is selected In case more than one response match request, the first one not yet sent (according to the registration order) will be sent. In case all matching responses have been sent once, the request will [not be considered as matched](#in-case-no-response-can-be-found) [(unless you turned `can_send_already_matched_responses` option on)](#allow-to-register-a-response-for-more-than-one-request). You can add criteria so that response will be sent only in case of a more specific matching. #### Matching on URL `url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. Matching is performed on the full URL, query parameters included. Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once. ```python import httpx import re from pytest_httpx import HTTPXMock def test_url(httpx_mock: HTTPXMock): httpx_mock.add_response(url="https://test_url?a=1&b=2") with httpx.Client() as client: response1 = client.delete("https://test_url?a=1&b=2") response2 = client.get("https://test_url?b=2&a=1") def test_url_as_pattern(httpx_mock: HTTPXMock): httpx_mock.add_response(url=re.compile(".*test.*")) with httpx.Client() as client: response = client.get("https://test_url") def test_url_as_httpx_url(httpx_mock: HTTPXMock): httpx_mock.add_response(url=httpx.URL("https://test_url", params={"a": "1", "b": "2"})) with httpx.Client() as client: response = client.get("https://test_url?a=1&b=2") ``` #### Matching on HTTP method Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) to reply to. `method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. Matching is performed on equality. ```python import httpx from pytest_httpx import HTTPXMock def test_post(httpx_mock: HTTPXMock): httpx_mock.add_response(method="POST") with httpx.Client() as client: response = client.post("https://test_url") def test_put(httpx_mock: HTTPXMock): httpx_mock.add_response(method="PUT") with httpx.Client() as client: response = client.put("https://test_url") def test_delete(httpx_mock: HTTPXMock): httpx_mock.add_response(method="DELETE") with httpx.Client() as client: response = client.delete("https://test_url") def test_patch(httpx_mock: HTTPXMock): httpx_mock.add_response(method="PATCH") with httpx.Client() as client: response = client.patch("https://test_url") def test_head(httpx_mock: HTTPXMock): httpx_mock.add_response(method="HEAD") with httpx.Client() as client: response = client.head("https://test_url") ``` #### Matching on proxy URL `proxy_url` parameter can either be a string, a python [re.Pattern](https://docs.python.org/3/library/re.html) instance or a [httpx.URL](https://www.python-httpx.org/api/#url) instance. Matching is performed on the full proxy URL, query parameters included. Order of parameters in the query string does not matter, however order of values do matter if the same parameter is provided more than once. ```python import httpx from pytest_httpx import HTTPXMock def test_proxy_url(httpx_mock: HTTPXMock): httpx_mock.add_response(proxy_url="http://test_proxy_url?b=1&a=2") with httpx.Client(proxy="http://test_proxy_url?a=2&b=1") as client: response = client.get("https://test_url") ``` #### Matching on HTTP headers Use `match_headers` parameter to specify the HTTP headers (as a dict) to reply to. Matching is performed on equality for each provided header. ```python import httpx from pytest_httpx import HTTPXMock def test_headers_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_headers={'User-Agent': 'python-httpx/0.25.0'}) with httpx.Client() as client: response = client.get("https://test_url") ``` #### Matching on HTTP body Use `match_content` parameter to specify the full HTTP body (as bytes) to reply to. Matching is performed on equality. ```python import httpx from pytest_httpx import HTTPXMock def test_content_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_content=b"This is the body") with httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") ``` ##### Matching on HTTP JSON body Use `match_json` parameter to specify the JSON decoded HTTP body to reply to. Matching is performed on equality. You can however use `unittest.mock.ANY` to do partial matching. ```python import httpx from pytest_httpx import HTTPXMock from unittest.mock import ANY def test_json_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_json={"a": "json", "b": 2}) with httpx.Client() as client: response = client.post("https://test_url", json={"a": "json", "b": 2}) def test_partial_json_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_json={"a": "json", "b": ANY}) with httpx.Client() as client: response = client.post("https://test_url", json={"a": "json", "b": 2}) ``` Note that `match_content` or `match_files` cannot be provided if `match_json` is also provided. ##### Matching on HTTP multipart body Use `match_files` and `match_data` parameters to specify the full multipart body to reply to. Matching is performed on equality. ```python import httpx from pytest_httpx import HTTPXMock def test_multipart_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}) with httpx.Client() as client: response = client.post("https://test_url", files={"name": ("file_name", b"File content")}, data={"field": "value"}) ``` Note that `match_content` or `match_json` cannot be provided if `match_files` is also provided. #### Matching on extensions Use `match_extensions` parameter to specify the extensions (as a dict) to reply to. Matching is performed on equality for each provided extension. ```python import httpx from pytest_httpx import HTTPXMock def test_extensions_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_extensions={'test': 'value'}) with httpx.Client() as client: response = client.get("https://test_url", extensions={"test": "value"}) ``` ##### Matching on HTTP timeout(s) Use `match_extensions` parameter to specify the timeouts (as a dict) to reply to. Matching is performed on the full timeout dict equality. ```python import httpx from pytest_httpx import HTTPXMock def test_timeout_matching(httpx_mock: HTTPXMock): httpx_mock.add_response(match_extensions={'timeout': {'connect': 10, 'read': 10, 'write': 10, 'pool': 10}}) with httpx.Client() as client: response = client.get("https://test_url", timeout=10) ``` ### Add JSON response Use `json` parameter to add a JSON response using python values. ```python import httpx from pytest_httpx import HTTPXMock def test_json(httpx_mock: HTTPXMock): httpx_mock.add_response(json=[{"key1": "value1", "key2": "value2"}]) with httpx.Client() as client: assert client.get("https://test_url").json() == [{"key1": "value1", "key2": "value2"}] ``` Note that the `content-type` header will be set to `application/json` by default in the response. ### Reply with custom body Use `text` parameter to reply with a custom body by providing UTF-8 encoded string. ```python import httpx from pytest_httpx import HTTPXMock def test_str_body(httpx_mock: HTTPXMock): httpx_mock.add_response(text="This is my UTF-8 content") with httpx.Client() as client: assert client.get("https://test_url").text == "This is my UTF-8 content" ``` Use `content` parameter to reply with a custom body by providing bytes. ```python import httpx from pytest_httpx import HTTPXMock def test_bytes_body(httpx_mock: HTTPXMock): httpx_mock.add_response(content=b"This is my bytes content") with httpx.Client() as client: assert client.get("https://test_url").content == b"This is my bytes content" ``` Use `html` parameter to reply with a custom body by providing UTF-8 encoded string. ```python import httpx from pytest_httpx import HTTPXMock def test_html_body(httpx_mock: HTTPXMock): httpx_mock.add_response(html="This is

HTML content") with httpx.Client() as client: assert client.get("https://test_url").text == "This is

HTML content" ``` ### Reply by streaming chunks Use `stream` parameter (as `httpx.SyncByteStream` or `httpx.AsyncByteStream`) to stream chunks that you specify. Note that `pytest_httpx.IteratorStream` can be used to provide an iterable. ```python import httpx import pytest from pytest_httpx import HTTPXMock, IteratorStream def test_sync_streaming(httpx_mock: HTTPXMock): httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) with httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1", b"part 2"] @pytest.mark.asyncio async def test_async_streaming(httpx_mock: HTTPXMock): httpx_mock.add_response(stream=IteratorStream([b"part 1", b"part 2"])) async with httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [b"part 1", b"part 2"] ``` ### Add multipart response Use the httpx `MultipartStream` via the `stream` parameter to send a multipart response. Reach out to `httpx` developers if you need this publicly exposed as [this is not a standard use case](https://github.com/encode/httpx/issues/872#issuecomment-633584819). ```python import httpx from httpx._multipart import MultipartStream from pytest_httpx import HTTPXMock def test_multipart_body(httpx_mock: HTTPXMock): httpx_mock.add_response(stream=MultipartStream(data={"key1": "value1"}, files={"file1": b"content of file 1"}, boundary=b"2256d3a36d2a61a1eba35a22bee5c74a")) with httpx.Client() as client: assert client.get("https://test_url").text == '''--2256d3a36d2a61a1eba35a22bee5c74a\r Content-Disposition: form-data; name="key1"\r \r value1\r --2256d3a36d2a61a1eba35a22bee5c74a\r Content-Disposition: form-data; name="file1"; filename="upload"\r Content-Type: application/octet-stream\r \r content of file 1\r --2256d3a36d2a61a1eba35a22bee5c74a--\r ''' ``` ### Add non 200 response Use `status_code` parameter to specify the HTTP status code (as an int) of the response. ```python import httpx from pytest_httpx import HTTPXMock def test_status_code(httpx_mock: HTTPXMock): httpx_mock.add_response(status_code=404) with httpx.Client() as client: assert client.get("https://test_url").status_code == 404 ``` ### Reply with custom headers Use `headers` parameter to specify the extra headers of the response. Any valid httpx headers type is supported, you can submit headers as a dict (str or bytes), a list of 2-tuples (str or bytes) or a [`httpx.Header`](https://www.python-httpx.org/api/#headers) instance. ```python import httpx from pytest_httpx import HTTPXMock def test_headers_as_str_dict(httpx_mock: HTTPXMock): httpx_mock.add_response(headers={"X-Header1": "Test value"}) with httpx.Client() as client: assert client.get("https://test_url").headers["x-header1"] == "Test value" def test_headers_as_str_tuple_list(httpx_mock: HTTPXMock): httpx_mock.add_response(headers=[("X-Header1", "Test value")]) with httpx.Client() as client: assert client.get("https://test_url").headers["x-header1"] == "Test value" def test_headers_as_httpx_headers(httpx_mock: HTTPXMock): httpx_mock.add_response(headers=httpx.Headers({b"X-Header1": b"Test value"})) with httpx.Client() as client: assert client.get("https://test_url").headers["x-header1"] == "Test value" ``` #### Reply with cookies Cookies are sent in the `set-cookie` HTTP header. You can then send cookies in the response by setting the `set-cookie` header with [the value following key=value format]((https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)). ```python import httpx from pytest_httpx import HTTPXMock def test_cookie(httpx_mock: HTTPXMock): httpx_mock.add_response(headers={"set-cookie": "key=value"}) with httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} def test_cookies(httpx_mock: HTTPXMock): httpx_mock.add_response(headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")]) with httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} ``` ### Add HTTP/2.0 response Use `http_version` parameter to specify the HTTP protocol version (as a string) of the response. ```python import httpx from pytest_httpx import HTTPXMock def test_http_version(httpx_mock: HTTPXMock): httpx_mock.add_response(http_version="HTTP/2.0") with httpx.Client() as client: assert client.get("https://test_url").http_version == "HTTP/2.0" ``` ## Add callbacks You can perform custom manipulation upon request reception by registering callbacks. Callback should expect one parameter, the received [`httpx.Request`](https://www.python-httpx.org/api/#request). If all callbacks are not executed during test execution, the test case will fail at teardown [(unless you turned `assert_all_responses_were_requested` option off)](#allow-to-register-more-responses-than-what-will-be-requested). Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected). Meaning that you can transpose `httpx_mock.add_response` calls in the related examples into `httpx_mock.add_callback`. ### Dynamic responses Callback should return a [`httpx.Response`](https://www.python-httpx.org/api/#response) instance. ```python import httpx from pytest_httpx import HTTPXMock def test_dynamic_response(httpx_mock: HTTPXMock): def custom_response(request: httpx.Request): return httpx.Response( status_code=200, json={"url": str(request.url)}, ) httpx_mock.add_callback(custom_response) with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == {"url": "https://test_url"} ``` Alternatively, callbacks can also be asynchronous. As in the following sample simulating network latency on some responses only. ```python import asyncio import httpx import pytest from pytest_httpx import HTTPXMock @pytest.mark.asyncio async def test_dynamic_async_response(httpx_mock: HTTPXMock): async def simulate_network_latency(request: httpx.Request): await asyncio.sleep(1) return httpx.Response( status_code=200, json={"url": str(request.url)}, ) httpx_mock.add_callback(simulate_network_latency) httpx_mock.add_response() async with httpx.AsyncClient() as client: responses = await asyncio.gather( # Response will be received after one second client.get("https://test_url"), # Response will instantly be received (1 second before the first request) client.get("https://test_url") ) ``` ### Raising exceptions You can simulate HTTPX exception throwing by raising an exception in your callback or use `httpx_mock.add_exception` with the exception instance. This can be useful if you want to assert that your code handles HTTPX exceptions properly. ```python import httpx import pytest from pytest_httpx import HTTPXMock def test_exception_raising(httpx_mock: HTTPXMock): httpx_mock.add_exception(httpx.ReadTimeout("Unable to read within timeout")) with httpx.Client() as client: with pytest.raises(httpx.ReadTimeout): client.get("https://test_url") ``` #### In case no response can be found The default behavior is to instantly raise a [`httpx.TimeoutException`](https://www.python-httpx.org/advanced/timeouts/) in case no matching response can be found. The exception message will display the request and every registered responses to help you identify any possible mismatch. ```python import httpx import pytest from pytest_httpx import HTTPXMock @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_timeout(httpx_mock: HTTPXMock): with httpx.Client() as client: with pytest.raises(httpx.TimeoutException): client.get("https://test_url") ``` ## Check sent requests The best way to ensure the content of your requests is still to use the `match_headers` and / or `match_content` parameters when adding a response. In the same spirit, ensuring that no request was issued does not necessarily require any code [(unless you turned `assert_all_requests_were_expected` option off)](#allow-to-not-register-responses-for-every-request). In any case, you always have the ability to retrieve the requests that were issued. As in the following samples: ```python import httpx from pytest_httpx import HTTPXMock def test_many_requests(httpx_mock: HTTPXMock): httpx_mock.add_response() with httpx.Client() as client: response1 = client.get("https://test_url") response2 = client.get("https://test_url") requests = httpx_mock.get_requests() def test_single_request(httpx_mock: HTTPXMock): httpx_mock.add_response() with httpx.Client() as client: response = client.get("https://test_url") request = httpx_mock.get_request() def test_no_request(httpx_mock: HTTPXMock): assert not httpx_mock.get_request() ``` ### How requests are selected You can add criteria so that requests will be returned only in case of a more specific matching. Note that requests are [selected the same way as responses](#how-response-is-selected). Meaning that you can transpose `httpx_mock.add_response` calls in the related examples into `httpx_mock.get_requests` or `httpx_mock.get_request`. ## Configuring httpx_mock The `httpx_mock` marker is available and can be used to change the default behavior of the `httpx_mock` fixture. Refer to [available options](#available-options) for an exhaustive list of options that can be set [per test](#per-test), [per module](#per-module) or even [on the whole test suite](#for-the-whole-test-suite). ### Per test ```python import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_something(httpx_mock): ... ``` ### Per module ```python import pytest pytestmark = pytest.mark.httpx_mock(assert_all_responses_were_requested=False) ``` ### For the whole test suite This should be set in the root `conftest.py` file. ```python import pytest def pytest_collection_modifyitems(session, config, items): for item in items: item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False)) ``` > [!IMPORTANT] > Note that [there currently is a bug in pytest](https://github.com/pytest-dev/pytest/issues/10406) where `pytest_collection_modifyitems` will actually add the marker AFTER its `module` and `class` registration. > > Meaning the order is currently: > module -> class -> test suite -> test > > instead of: > test suite -> module -> class -> test ### Available options #### Allow to register more responses than what will be requested By default, `pytest-httpx` will ensure that every response was requested during test execution. If you want to add an optional response, you can use the `is_optional` parameter when [registering a response](#add-responses) or [a callback](#add-callbacks). ```python def test_fewer_requests_than_expected(httpx_mock): # Even if this response never received a corresponding request, the test will not fail at teardown httpx_mock.add_response(is_optional=True) ``` If you don't have control over the response registration process (shared fixtures), and you want to allow fewer requests than what you registered responses for, you can use the `httpx_mock` marker `assert_all_responses_were_requested` option. > [!CAUTION] > Use this option at your own risk of not spotting regression (requests not sent) in your code base! ```python import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_fewer_requests_than_expected(httpx_mock): # Even if this response never received a corresponding request, the test will not fail at teardown httpx_mock.add_response() ``` Note that the `is_optional` parameter will take precedence over the `assert_all_responses_were_requested` option. Meaning you can still register a response that will be checked for execution at teardown even if `assert_all_responses_were_requested` was set to `False`. ```python import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_force_expected_request(httpx_mock): # Even if the assert_all_responses_were_requested option is set, the test will fail at teardown if this is not matched httpx_mock.add_response(is_optional=False) ``` #### Allow to not register responses for every request By default, `pytest-httpx` will ensure that every request that was issued was expected. You can use the `httpx_mock` marker `assert_all_requests_were_expected` option to allow more requests than what you registered responses for. > [!CAUTION] > Use this option at your own risk of not spotting regression (unexpected requests) in your code base! ```python import pytest import httpx @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_more_requests_than_expected(httpx_mock): with httpx.Client() as client: # Even if this request was not expected, the test will not fail at teardown with pytest.raises(httpx.TimeoutException): client.get("https://test_url") ``` #### Allow to register a response for more than one request By default, `pytest-httpx` will ensure that every request that was issued was expected. If you want to add a response once, while allowing it to match more than once, you can use the `is_reusable` parameter when [registering a response](#add-responses) or [a callback](#add-callbacks). ```python import httpx def test_more_requests_than_responses(httpx_mock): httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url") # Even if only one response was registered, the test will not fail at teardown as this request will also be matched client.get("https://test_url") ``` If you don't have control over the response registration process (shared fixtures), and you want to allow multiple requests to match the same registered response, you can use the `httpx_mock` marker `can_send_already_matched_responses` option. With this option, in case all matching responses have been sent at least once, the last one (according to the registration order) will be sent. > [!CAUTION] > Use this option at your own risk of not spotting regression (requests issued more than the expected number of times) in your code base! ```python import pytest import httpx @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_more_requests_than_responses(httpx_mock): httpx_mock.add_response() with httpx.Client() as client: client.get("https://test_url") # Even if only one response was registered, the test will not fail at teardown as this request will also be matched client.get("https://test_url") ``` #### Do not mock some requests By default, `pytest-httpx` will mock every request. But, for instance, in case you want to write integration tests with other servers, you might want to let some requests go through. To do so, you can use the `httpx_mock` marker `should_mock` option and provide a callable expecting the [`httpx.Request`](https://www.python-httpx.org/api/#request) as parameter and returning a boolean. Returning `True` will ensure that the request is handled by `pytest-httpx` (mocked), `False` will let the request pass through (not mocked). ```python import pytest import httpx @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "www.my_local_test_host") def test_partial_mock(httpx_mock): httpx_mock.add_response() with httpx.Client() as client: # This request will NOT be mocked response1 = client.get("https://www.my_local_test_host/sub?param=value") # This request will be mocked response2 = client.get("https://test_url") ``` ## Migrating to pytest-httpx Here is how to migrate from well-known testing libraries to `pytest-httpx`. ### From responses | Feature | responses | pytest-httpx | |:------------------|:---------------------------|:----------------------------| | Add a response | `responses.add()` | `httpx_mock.add_response()` | | Add a callback | `responses.add_callback()` | `httpx_mock.add_callback()` | | Retrieve requests | `responses.calls` | `httpx_mock.get_requests()` | #### Add a response or a callback Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. Below is a list of parameters that will require a change in your code. | Parameter | responses | pytest-httpx | |:---------------------|:------------------------------------|:---------------------------------------------------------------------| | method | `method=responses.GET` | `method="GET"` | | body (as bytes) | `body=b"sample"` | `content=b"sample"` | | body (as str) | `body="sample"` | `text="sample"` | | status code | `status=201` | `status_code=201` | | headers | `adding_headers={"name": "value"}` | `headers={"name": "value"}` | | content-type header | `content_type="application/custom"` | `headers={"content-type": "application/custom"}` | | Match the full query | `match_querystring=True` | The full query is always matched when providing the `url` parameter. | Sample adding a response with `responses`: ```python from responses import RequestsMock def test_response(responses: RequestsMock): responses.add( method=responses.GET, url="https://test_url", body=b"This is the response content", status=400, ) ``` Sample adding the same response with `pytest-httpx`: ```python from pytest_httpx import HTTPXMock def test_response(httpx_mock: HTTPXMock): httpx_mock.add_response( method="GET", url="https://test_url", content=b"This is the response content", status_code=400, ) ``` ### From aioresponses | Feature | aioresponses | pytest-httpx | |:---------------|:------------------------|:-------------------------------------------| | Add a response | `aioresponses.method()` | `httpx_mock.add_response(method="METHOD")` | | Add a callback | `aioresponses.method()` | `httpx_mock.add_callback(method="METHOD")` | #### Add a response or a callback Undocumented parameters means that they are unchanged between `responses` and `pytest-httpx`. Below is a list of parameters that will require a change in your code. | Parameter | responses | pytest-httpx | |:----------------|:---------------------|:--------------------| | body (as bytes) | `body=b"sample"` | `content=b"sample"` | | body (as str) | `body="sample"` | `text="sample"` | | body (as JSON) | `payload=["sample"]` | `json=["sample"]` | | status code | `status=201` | `status_code=201` | Sample adding a response with `aioresponses`: ```python import pytest from aioresponses import aioresponses @pytest.fixture def mock_aioresponse(): with aioresponses() as m: yield m def test_response(mock_aioresponse): mock_aioresponse.get( url="https://test_url", body=b"This is the response content", status=400, ) ``` Sample adding the same response with `pytest-httpx`: ```python def test_response(httpx_mock): httpx_mock.add_response( method="GET", url="https://test_url", content=b"This is the response content", status_code=400, ) ``` pytest_httpx-0.35.0/_config.yml000066400000000000000000000000321472214060000164740ustar00rootroot00000000000000theme: jekyll-theme-caymanpytest_httpx-0.35.0/pyproject.toml000066400000000000000000000036101472214060000172660ustar00rootroot00000000000000[build-system] requires = ["setuptools", "setuptools_scm"] build-backend = "setuptools.build_meta" [project] name = "pytest-httpx" description = "Send responses to httpx." readme = "README.md" requires-python = ">=3.9" license = {file = "LICENSE"} authors = [ { name = "Colin Bounouar", email = "colin.bounouar.dev@gmail.com" }, ] maintainers = [ { name = "Colin Bounouar", email = "colin.bounouar.dev@gmail.com" }, ] keywords = [ "httpx", "pytest", "testing", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Build Tools", "Typing :: Typed", ] dependencies = [ "httpx==0.28.*", "pytest==8.*", ] dynamic = ["version"] [project.urls] documentation = "https://colin-b.github.io/pytest_httpx/" repository = "https://github.com/Colin-b/pytest_httpx" changelog = "https://github.com/Colin-b/pytest_httpx/blob/master/CHANGELOG.md" issues = "https://github.com/Colin-b/pytest_httpx/issues" [project.optional-dependencies] testing = [ # Used to check coverage "pytest-cov==6.*", # Used to run async tests "pytest-asyncio==0.24.*", ] [project.entry-points.pytest11] pytest_httpx = "pytest_httpx" [tool.setuptools.dynamic] version = {attr = "pytest_httpx.version.__version__"} [tool.pytest.ini_options] # Silence deprecation warnings about option "asyncio_default_fixture_loop_scope" asyncio_default_fixture_loop_scope = "function" pytest_httpx-0.35.0/pytest_httpx/000077500000000000000000000000001472214060000171315ustar00rootroot00000000000000pytest_httpx-0.35.0/pytest_httpx/__init__.py000066400000000000000000000043451472214060000212500ustar00rootroot00000000000000from collections.abc import Generator from operator import methodcaller import httpx import pytest from pytest import Config, FixtureRequest, MonkeyPatch from pytest_httpx._httpx_mock import HTTPXMock from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx.version import __version__ __all__ = ( "HTTPXMock", "IteratorStream", "__version__", ) @pytest.fixture def httpx_mock( monkeypatch: MonkeyPatch, request: FixtureRequest, ) -> Generator[HTTPXMock, None, None]: options = {} for marker in request.node.iter_markers("httpx_mock"): options = marker.kwargs | options __tracebackhide__ = methodcaller("errisinstance", TypeError) options = _HTTPXMockOptions(**options) mock = HTTPXMock(options) # Mock synchronous requests real_handle_request = httpx.HTTPTransport.handle_request def mocked_handle_request( transport: httpx.HTTPTransport, request: httpx.Request ) -> httpx.Response: if options.should_mock(request): return mock._handle_request(transport, request) return real_handle_request(transport, request) monkeypatch.setattr( httpx.HTTPTransport, "handle_request", mocked_handle_request, ) # Mock asynchronous requests real_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request async def mocked_handle_async_request( transport: httpx.AsyncHTTPTransport, request: httpx.Request ) -> httpx.Response: if options.should_mock(request): return await mock._handle_async_request(transport, request) return await real_handle_async_request(transport, request) monkeypatch.setattr( httpx.AsyncHTTPTransport, "handle_async_request", mocked_handle_async_request, ) yield mock try: mock._assert_options() finally: mock.reset() def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "httpx_mock(*, assert_all_responses_were_requested=True, assert_all_requests_were_expected=True, can_send_already_matched_responses=False, should_mock=lambda request: True): Configure httpx_mock fixture.", ) pytest_httpx-0.35.0/pytest_httpx/_httpx_internals.py000066400000000000000000000034421472214060000230730ustar00rootroot00000000000000import base64 from typing import Union, Optional from collections.abc import Sequence, Iterable, AsyncIterator, Iterator import httpcore import httpx # TODO Get rid of this internal import from httpx._content import IteratorByteStream, AsyncIteratorByteStream # Those types are internally defined within httpx._types HeaderTypes = Union[ httpx.Headers, dict[str, str], dict[bytes, bytes], Sequence[tuple[str, str]], Sequence[tuple[bytes, bytes]], ] class IteratorStream(AsyncIteratorByteStream, IteratorByteStream): def __init__(self, stream: Iterable[bytes]): class Stream: def __iter__(self) -> Iterator[bytes]: yield from stream async def __aiter__(self) -> AsyncIterator[bytes]: for chunk in stream: yield chunk AsyncIteratorByteStream.__init__(self, stream=Stream()) IteratorByteStream.__init__(self, stream=Stream()) def _to_httpx_url(url: httpcore.URL, headers: list[tuple[bytes, bytes]]) -> httpx.URL: for name, value in headers: if b"Proxy-Authorization" == name: return httpx.URL( scheme=url.scheme.decode(), host=url.host.decode(), port=url.port, raw_path=url.target, userinfo=base64.b64decode(value[6:]), ) return httpx.URL( scheme=url.scheme.decode(), host=url.host.decode(), port=url.port, raw_path=url.target, ) def _proxy_url( real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport] ) -> Optional[httpx.URL]: if isinstance( real_pool := real_transport._pool, (httpcore.HTTPProxy, httpcore.AsyncHTTPProxy) ): return _to_httpx_url(real_pool._proxy_url, real_pool._proxy_headers) pytest_httpx-0.35.0/pytest_httpx/_httpx_mock.py000066400000000000000000000422251472214060000220270ustar00rootroot00000000000000import copy import inspect from typing import Union, Optional, Callable, Any, NoReturn from collections.abc import Awaitable import httpx from pytest_httpx import _httpx_internals from pytest_httpx._options import _HTTPXMockOptions from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher class HTTPXMock: """ This class is only exposed for `httpx_mock` fixture type hinting purpose. """ def __init__(self, options: _HTTPXMockOptions) -> None: """Private and subject to breaking changes without notice.""" self._options = options self._requests: list[ tuple[Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], httpx.Request] ] = [] self._callbacks: list[ tuple[ _RequestMatcher, Callable[ [httpx.Request], Union[ Optional[httpx.Response], Awaitable[Optional[httpx.Response]] ], ], ] ] = [] self._requests_not_matched: list[httpx.Request] = [] def add_response( self, status_code: int = 200, http_version: str = "HTTP/1.1", headers: Optional[_httpx_internals.HeaderTypes] = None, content: Optional[bytes] = None, text: Optional[str] = None, html: Optional[str] = None, stream: Any = None, json: Any = None, **matchers: Any, ) -> None: """ Mock the response that will be sent if a request match. :param status_code: HTTP status code of the response to send. Default to 200 (OK). :param http_version: HTTP protocol version of the response to send. Default to HTTP/1.1 :param headers: HTTP headers of the response to send. Default to no headers. :param content: HTTP body of the response (as bytes). :param text: HTTP body of the response (as string). :param html: HTTP body of the response (as HTML string content). :param stream: HTTP body of the response (as httpx.SyncByteStream or httpx.AsyncByteStream) as stream content. :param json: HTTP body of the response (if JSON should be used as content type) if data is not provided. :param url: Full URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance. :param method: HTTP method identifying the request(s) to match. :param proxy_url: Full proxy URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance. :param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary. :param match_content: Full HTTP body identifying the request(s) to match. Must be bytes. :param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable. :param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary. :param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding :param is_optional: True will mark this response as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False). :param is_reusable: True will allow re-using this response even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False). """ json = copy.deepcopy(json) if json is not None else None def response_callback(request: httpx.Request) -> httpx.Response: return httpx.Response( status_code=status_code, extensions={"http_version": http_version.encode("ascii")}, headers=headers, json=json, content=content, text=text, html=html, stream=stream, ) self.add_callback(response_callback, **matchers) def add_callback( self, callback: Callable[ [httpx.Request], Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]], ], **matchers: Any, ) -> None: """ Mock the action that will take place if a request match. :param callback: The callable that will be called upon reception of the matched request. It must expect one parameter, the received httpx.Request and should return a httpx.Response. :param url: Full URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance. :param method: HTTP method identifying the request(s) to match. :param proxy_url: Full proxy URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance. :param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary. :param match_content: Full HTTP body identifying the request(s) to match. Must be bytes. :param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable. :param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary. :param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding :param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary. :param is_optional: True will mark this callback as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False). :param is_reusable: True will allow re-using this callback even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False). """ self._callbacks.append((_RequestMatcher(self._options, **matchers), callback)) def add_exception(self, exception: Exception, **matchers: Any) -> None: """ Raise an exception if a request match. :param exception: The exception that will be raised upon reception of the matched request. :param url: Full URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance. :param method: HTTP method identifying the request(s) to match. :param proxy_url: Full proxy URL identifying the request(s) to match. Can be a str, a re.Pattern instance or a httpx.URL instance. :param match_headers: HTTP headers identifying the request(s) to match. Must be a dictionary. :param match_content: Full HTTP body identifying the request(s) to match. Must be bytes. :param match_json: JSON decoded HTTP body identifying the request(s) to match. Must be JSON encodable. :param match_data: Multipart data (excluding files) identifying the request(s) to match. Must be a dictionary. :param match_files: Multipart files identifying the request(s) to match. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding :param match_extensions: Extensions identifying the request(s) to match. Must be a dictionary. :param is_optional: True will mark this exception response as optional, False will expect a request matching it. Must be a boolean. Default to the opposite of assert_all_responses_were_requested option value (itself defaulting to True, meaning this parameter default to False). :param is_reusable: True will allow re-using this exception response even if it already matched, False prevent re-using it. Must be a boolean. Default to the can_send_already_matched_responses option value (itself defaulting to False). """ def exception_callback(request: httpx.Request) -> None: if isinstance(exception, httpx.RequestError): exception.request = request raise exception self.add_callback(exception_callback, **matchers) def _handle_request( self, real_transport: httpx.HTTPTransport, request: httpx.Request, ) -> httpx.Response: # Store the content in request for future matching request.read() self._requests.append((real_transport, request)) callback = self._get_callback(real_transport, request) if callback: response = callback(request) if response: return _unread(response) self._request_not_matched(real_transport, request) async def _handle_async_request( self, real_transport: httpx.AsyncHTTPTransport, request: httpx.Request, ) -> httpx.Response: # Store the content in request for future matching await request.aread() self._requests.append((real_transport, request)) callback = self._get_callback(real_transport, request) if callback: response = callback(request) if response: if inspect.isawaitable(response): response = await response return _unread(response) self._request_not_matched(real_transport, request) def _request_not_matched( self, real_transport: Union[httpx.AsyncHTTPTransport, httpx.HTTPTransport], request: httpx.Request, ) -> NoReturn: self._requests_not_matched.append(request) raise httpx.TimeoutException( self._explain_that_no_response_was_found(real_transport, request), request=request, ) def _explain_that_no_response_was_found( self, real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport], request: httpx.Request, ) -> str: matchers = [matcher for matcher, _ in self._callbacks] message = f"No response can be found for {RequestDescription(real_transport, request, matchers)}" already_matched = [] unmatched = [] for matcher in matchers: if matcher.nb_calls: already_matched.append(matcher) else: unmatched.append(matcher) matchers_description = "\n".join( [f"- {matcher}" for matcher in unmatched + already_matched] ) if matchers_description: message += f" amongst:\n{matchers_description}" # If we could not find a response, but we have already matched responses # it might be that user is expecting one of those responses to be reused if any(not matcher.is_reusable for matcher in already_matched): message += "\n\nIf you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request" return message def _get_callback( self, real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], request: httpx.Request, ) -> Optional[ Callable[ [httpx.Request], Union[Optional[httpx.Response], Awaitable[Optional[httpx.Response]]], ] ]: callbacks = [ (matcher, callback) for matcher, callback in self._callbacks if matcher.match(real_transport, request) ] # No callback match this request if not callbacks: return None # Callbacks match this request for matcher, callback in callbacks: # Return the first not yet called if not matcher.nb_calls: matcher.nb_calls += 1 return callback # Or the last registered (if it can be reused) if matcher.is_reusable: matcher.nb_calls += 1 return callback # All callbacks have already been matched and last registered cannot be reused return None def get_requests(self, **matchers: Any) -> list[httpx.Request]: """ Return all requests sent that match (empty list if no requests were matched). :param url: Full URL identifying the requests to retrieve. Can be a str, a re.Pattern instance or a httpx.URL instance. :param method: HTTP method identifying the requests to retrieve. Must be an upper-cased string value. :param proxy_url: Full proxy URL identifying the requests to retrieve. Can be a str, a re.Pattern instance or a httpx.URL instance. :param match_headers: HTTP headers identifying the requests to retrieve. Must be a dictionary. :param match_content: Full HTTP body identifying the requests to retrieve. Must be bytes. :param match_json: JSON decoded HTTP body identifying the requests to retrieve. Must be JSON encodable. :param match_data: Multipart data (excluding files) identifying the requests to retrieve. Must be a dictionary. :param match_files: Multipart files identifying the requests to retrieve. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding :param match_extensions: Extensions identifying the requests to retrieve. Must be a dictionary. """ matcher = _RequestMatcher(self._options, **matchers) return [ request for real_transport, request in self._requests if matcher.match(real_transport, request) ] def get_request(self, **matchers: Any) -> Optional[httpx.Request]: """ Return the single request that match (or None). :param url: Full URL identifying the request to retrieve. Can be a str, a re.Pattern instance or a httpx.URL instance. :param method: HTTP method identifying the request to retrieve. Must be an upper-cased string value. :param proxy_url: Full proxy URL identifying the request to retrieve. Can be a str, a re.Pattern instance or a httpx.URL instance. :param match_headers: HTTP headers identifying the request to retrieve. Must be a dictionary. :param match_content: Full HTTP body identifying the request to retrieve. Must be bytes. :param match_json: JSON decoded HTTP body identifying the request to retrieve. Must be JSON encodable. :param match_data: Multipart data (excluding files) identifying the request to retrieve. Must be a dictionary. :param match_files: Multipart files identifying the request to retrieve. Refer to httpx documentation for more information on supported values: https://www.python-httpx.org/advanced/clients/#multipart-file-encoding :param match_extensions: Extensions identifying the request to retrieve. Must be a dictionary. :raises AssertionError: in case more than one request match. """ requests = self.get_requests(**matchers) assert ( len(requests) <= 1 ), f"More than one request ({len(requests)}) matched, use get_requests instead or refine your filters." return requests[0] if requests else None def reset(self) -> None: self._requests.clear() self._callbacks.clear() self._requests_not_matched.clear() def _assert_options(self) -> None: callbacks_not_executed = [ matcher for matcher, _ in self._callbacks if matcher.should_have_matched() ] matchers_description = "\n".join( [f"- {matcher}" for matcher in callbacks_not_executed] ) assert not callbacks_not_executed, ( "The following responses are mocked but not requested:\n" f"{matchers_description}\n" "\n" "If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested" ) if self._options.assert_all_requests_were_expected: requests_description = "\n".join( [ f"- {request.method} request on {request.url}" for request in self._requests_not_matched ] ) assert not self._requests_not_matched, ( f"The following requests were not expected:\n" f"{requests_description}\n" "\n" "If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request" ) def _unread(response: httpx.Response) -> httpx.Response: # Allow to read the response on client side response.is_stream_consumed = False response.is_closed = False if hasattr(response, "_content"): del response._content return response pytest_httpx-0.35.0/pytest_httpx/_options.py000066400000000000000000000012361472214060000213370ustar00rootroot00000000000000from typing import Callable import httpx class _HTTPXMockOptions: def __init__( self, *, assert_all_responses_were_requested: bool = True, assert_all_requests_were_expected: bool = True, can_send_already_matched_responses: bool = False, should_mock: Callable[[httpx.Request], bool] = lambda request: True, ) -> None: self.assert_all_responses_were_requested = assert_all_responses_were_requested self.assert_all_requests_were_expected = assert_all_requests_were_expected self.can_send_already_matched_responses = can_send_already_matched_responses self.should_mock = should_mock pytest_httpx-0.35.0/pytest_httpx/_pretty_print.py000066400000000000000000000054231472214060000224110ustar00rootroot00000000000000from typing import Union import httpx from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._request_matcher import _RequestMatcher class RequestDescription: def __init__( self, real_transport: Union[httpx.BaseTransport, httpx.AsyncBaseTransport], request: httpx.Request, matchers: list[_RequestMatcher], ): self.real_transport = real_transport self.request = request headers_encoding = request.headers.encoding self.expected_headers = { # httpx uses lower cased header names as internal key header.lower().encode(headers_encoding) for matcher in matchers if matcher.headers for header in matcher.headers } self.expect_body = any([matcher.expect_body() for matcher in matchers]) self.expect_proxy = any([matcher.proxy_url is not None for matcher in matchers]) self.expected_extensions = { extension for matcher in matchers if matcher.extensions for extension in matcher.extensions } def __str__(self) -> str: request_description = f"{self.request.method} request on {self.request.url}" if extra_description := self.extra_request_description(): request_description += f" with {extra_description}" return request_description def extra_request_description(self) -> str: extra_description = [] if self.expected_headers: headers_encoding = self.request.headers.encoding present_headers = {} # Can be cleaned based on the outcome of https://github.com/encode/httpx/discussions/2841 for name, lower_name, value in self.request.headers._list: if lower_name in self.expected_headers: name = name.decode(headers_encoding) if name in present_headers: present_headers[name] += f", {value.decode(headers_encoding)}" else: present_headers[name] = value.decode(headers_encoding) extra_description.append(f"{present_headers} headers") if self.expect_body: extra_description.append(f"{self.request.read()} body") if self.expect_proxy: proxy_url = _proxy_url(self.real_transport) extra_description.append(f"{proxy_url if proxy_url else 'no'} proxy URL") if self.expected_extensions: present_extensions = { name: value for name, value in self.request.extensions.items() if name in self.expected_extensions } extra_description.append(f"{present_extensions} extensions") return " and ".join(extra_description) pytest_httpx-0.35.0/pytest_httpx/_request_matcher.py000066400000000000000000000176671472214060000230560ustar00rootroot00000000000000import json import re from typing import Optional, Union, Any from re import Pattern import httpx from pytest_httpx._httpx_internals import _proxy_url from pytest_httpx._options import _HTTPXMockOptions def _url_match( url_to_match: Union[Pattern[str], httpx.URL], received: httpx.URL ) -> bool: if isinstance(url_to_match, re.Pattern): return url_to_match.match(str(received)) is not None # Compare query parameters apart as order of parameters should not matter received_params = dict(received.params) params = dict(url_to_match.params) # Remove the query parameters from the original URL to compare everything besides query parameters received_url = received.copy_with(query=None) url = url_to_match.copy_with(query=None) return (received_params == params) and (url == received_url) class _RequestMatcher: def __init__( self, options: _HTTPXMockOptions, url: Optional[Union[str, Pattern[str], httpx.URL]] = None, method: Optional[str] = None, proxy_url: Optional[Union[str, Pattern[str], httpx.URL]] = None, match_headers: Optional[dict[str, Any]] = None, match_content: Optional[bytes] = None, match_json: Optional[Any] = None, match_data: Optional[dict[str, Any]] = None, match_files: Optional[Any] = None, match_extensions: Optional[dict[str, Any]] = None, is_optional: Optional[bool] = None, is_reusable: Optional[bool] = None, ): self._options = options self.nb_calls = 0 self.url = httpx.URL(url) if url and isinstance(url, str) else url self.method = method.upper() if method else method self.headers = match_headers self.content = match_content self.json = match_json self.data = match_data self.files = match_files self.proxy_url = ( httpx.URL(proxy_url) if proxy_url and isinstance(proxy_url, str) else proxy_url ) self.extensions = match_extensions self.is_optional = not options.assert_all_responses_were_requested if is_optional is None else is_optional self.is_reusable = options.can_send_already_matched_responses if is_reusable is None else is_reusable if self._is_matching_body_more_than_one_way(): raise ValueError( "Only one way of matching against the body can be provided. " "If you want to match against the JSON decoded representation, use match_json. " "If you want to match against the multipart representation, use match_files (and match_data). " "Otherwise, use match_content." ) if self.data and not self.files: raise ValueError( "match_data is meant to be used for multipart matching (in conjunction with match_files)." "Use match_content to match url encoded data." ) def expect_body(self) -> bool: matching_ways = [ self.content is not None, self.json is not None, self.files is not None, ] return sum(matching_ways) == 1 def _is_matching_body_more_than_one_way(self) -> bool: matching_ways = [ self.content is not None, self.json is not None, self.files is not None, ] return sum(matching_ways) > 1 def match( self, real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport], request: httpx.Request, ) -> bool: return ( self._url_match(request) and self._method_match(request) and self._headers_match(request) and self._content_match(request) and self._proxy_match(real_transport) and self._extensions_match(request) ) def _url_match(self, request: httpx.Request) -> bool: if not self.url: return True return _url_match(self.url, request.url) def _method_match(self, request: httpx.Request) -> bool: if not self.method: return True return request.method == self.method def _headers_match(self, request: httpx.Request) -> bool: if not self.headers: return True encoding = request.headers.encoding request_headers = {} # Can be cleaned based on the outcome of https://github.com/encode/httpx/discussions/2841 for raw_name, raw_value in request.headers.raw: if raw_name in request_headers: request_headers[raw_name] += b", " + raw_value else: request_headers[raw_name] = raw_value return all( request_headers.get(header_name.encode(encoding)) == header_value.encode(encoding) for header_name, header_value in self.headers.items() ) def _content_match(self, request: httpx.Request) -> bool: if self.content is not None: return request.content == self.content if self.json is not None: try: # httpx._content.encode_json hard codes utf-8 encoding. return json.loads(request.content.decode("utf-8")) == self.json except json.decoder.JSONDecodeError: return False if self.files: if not ( boundary_matched := re.match(b"^--([0-9a-f]*)\r\n", request.content) ): return False # Ensure we re-use the same boundary for comparison boundary = boundary_matched.group(1) # Prevent internal httpx changes from impacting users not matching on files from httpx._multipart import MultipartStream multipart_content = b"".join( MultipartStream(self.data or {}, self.files, boundary) ) return request.content == multipart_content return True def _proxy_match( self, real_transport: Union[httpx.HTTPTransport, httpx.AsyncHTTPTransport] ) -> bool: if not self.proxy_url: return True if real_proxy_url := _proxy_url(real_transport): return _url_match(self.proxy_url, real_proxy_url) return False def _extensions_match(self, request: httpx.Request) -> bool: if not self.extensions: return True return all( request.extensions.get(extension_name) == extension_value for extension_name, extension_value in self.extensions.items() ) def should_have_matched(self) -> bool: """Return True if the matcher did not serve its purpose.""" return not self.is_optional and not self.nb_calls def __str__(self) -> str: if self.is_reusable: matcher_description = f"Match {self.method or 'every'} request" else: matcher_description = "Already matched" if self.nb_calls else "Match" matcher_description += f" {self.method or 'any'} request" if self.url: matcher_description += f" on {self.url}" if extra_description := self._extra_description(): matcher_description += f" with {extra_description}" return matcher_description def _extra_description(self) -> str: extra_description = [] if self.headers: extra_description.append(f"{self.headers} headers") if self.content is not None: extra_description.append(f"{self.content} body") if self.json is not None: extra_description.append(f"{self.json} json body") if self.data is not None: extra_description.append(f"{self.data} multipart data") if self.files is not None: extra_description.append(f"{self.files} files") if self.proxy_url: extra_description.append(f"{self.proxy_url} proxy URL") if self.extensions: extra_description.append(f"{self.extensions} extensions") return " and ".join(extra_description) pytest_httpx-0.35.0/pytest_httpx/py.typed000066400000000000000000000000001472214060000206160ustar00rootroot00000000000000pytest_httpx-0.35.0/pytest_httpx/version.py000066400000000000000000000005641472214060000211750ustar00rootroot00000000000000# Version number as Major.Minor.Patch # The version modification must respect the following rules: # Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0) # Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0) # Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9) __version__ = "0.35.0" pytest_httpx-0.35.0/tests/000077500000000000000000000000001472214060000155145ustar00rootroot00000000000000pytest_httpx-0.35.0/tests/__init__.py000066400000000000000000000000001472214060000176130ustar00rootroot00000000000000pytest_httpx-0.35.0/tests/conftest.py000066400000000000000000000002051472214060000177100ustar00rootroot00000000000000# see https://docs.pytest.org/en/documentation-restructure/how-to/writing_plugins.html#testing-plugins pytest_plugins = ["pytester"] pytest_httpx-0.35.0/tests/test_httpx_async.py000066400000000000000000002704701472214060000215030ustar00rootroot00000000000000import asyncio import math import os import re import time from collections.abc import AsyncIterable import httpx import pytest from pytest import Testdir from unittest.mock import ANY import pytest_httpx from pytest_httpx import HTTPXMock @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_without_response(httpx_mock: HTTPXMock) -> None: with pytest.raises(Exception) as exception_info: async with httpx.AsyncClient() as client: await client.get("https://test_url") assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url""" ) @pytest.mark.asyncio async def test_default_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" assert response.status_code == 200 assert response.headers == httpx.Headers({}) assert response.http_version == "HTTP/1.1" @pytest.mark.asyncio async def test_url_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio async def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" response = await client.post("https://test_url") assert response.content == b"" @pytest.mark.asyncio async def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&b=2", is_reusable=True) async with httpx.AsyncClient() as client: response = await client.post("https://test_url?a=1&b=2") assert response.content == b"" # Parameters order should not matter response = await client.get("https://test_url?b=2&a=1") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("https://test_url2") assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url2 amongst: - Match any request on https://test_url""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2", is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: # Same parameter order matters as it corresponds to a list on server side await client.get("https://test_url?a=2&a=1") assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url?a=2&a=1 amongst: - Match any request on https://test_url?a=1&a=2""" ) @pytest.mark.asyncio async def test_method_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get", is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" response = await client.get("https://test_url2") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get", is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url") assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url amongst: - Match GET request""" ) @pytest.mark.asyncio async def test_reusing_one_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content", is_reusable=True ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content" response = await client.get("https://test_url") assert response.content == b"test content" @pytest.mark.asyncio async def test_response_with_string_body(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", text="test content") async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content" @pytest.mark.asyncio async def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", html="test content") async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content" @pytest.mark.asyncio async def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", stream=pytest_httpx.IteratorStream([b"part 1", b"part 2"]), is_reusable=True, ) async with httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1", b"part 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1", b"part 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio async def test_content_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", content=b"part 1 and 2", is_reusable=True, ) async with httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio async def test_text_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", text="part 1 and 2", is_reusable=True, ) async with httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [ b"part 1 and 2", ] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio async def test_default_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover async with client.stream(method="GET", url="https://test_url") as response: assert [part async for part in response.aiter_raw()] == [] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): async for _ in response.aiter_raw(): pass # pragma: no cover @pytest.mark.asyncio async def test_with_many_responses(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response(url="https://test_url", content=b"test content 2") httpx_mock.add_response(url="https://test_url", content=b"test content 2") async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" response = await client.get("https://test_url") assert response.content == b"test content 2" response = await client.get("https://test_url") assert response.content == b"test content 2" @pytest.mark.asyncio async def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response( url="https://test_url", content=b"test content 2", is_reusable=True ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" response = await client.get("https://test_url") assert response.content == b"test content 2" response = await client.get("https://test_url") assert response.content == b"test content 2" @pytest.mark.asyncio async def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) httpx_mock.add_response( url="https://test_url", method="POST", content=b"test content 2" ) httpx_mock.add_response( url="https://test_url", method="PUT", content=b"test content 3" ) httpx_mock.add_response( url="https://test_url", method="DELETE", content=b"test content 4" ) httpx_mock.add_response( url="https://test_url", method="PATCH", content=b"test content 5" ) httpx_mock.add_response( url="https://test_url", method="HEAD", content=b"test content 6" ) async with httpx.AsyncClient() as client: response = await client.post("https://test_url") assert response.content == b"test content 2" response = await client.get("https://test_url") assert response.content == b"test content 1" response = await client.put("https://test_url") assert response.content == b"test content 3" response = await client.head("https://test_url") assert response.content == b"test content 6" response = await client.patch("https://test_url") assert response.content == b"test content 5" response = await client.delete("https://test_url") assert response.content == b"test content 4" @pytest.mark.asyncio async def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1", status_code=200 ) httpx_mock.add_response( url="https://test_url", method="POST", content=b"test content 2", status_code=201, ) httpx_mock.add_response( url="https://test_url", method="PUT", content=b"test content 3", status_code=202 ) httpx_mock.add_response( url="https://test_url", method="DELETE", content=b"test content 4", status_code=303, ) httpx_mock.add_response( url="https://test_url", method="PATCH", content=b"test content 5", status_code=404, ) httpx_mock.add_response( url="https://test_url", method="HEAD", content=b"test content 6", status_code=500, ) async with httpx.AsyncClient() as client: response = await client.post("https://test_url") assert response.content == b"test content 2" assert response.status_code == 201 response = await client.get("https://test_url") assert response.content == b"test content 1" assert response.status_code == 200 response = await client.put("https://test_url") assert response.content == b"test content 3" assert response.status_code == 202 response = await client.head("https://test_url") assert response.content == b"test content 6" assert response.status_code == 500 response = await client.patch("https://test_url") assert response.content == b"test content 5" assert response.status_code == 404 response = await client.delete("https://test_url") assert response.content == b"test content 4" assert response.status_code == 303 @pytest.mark.asyncio async def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url?param1=test", method="GET", content=b"test content 1" ) httpx_mock.add_response( url="https://test_url?param2=test", method="POST", content=b"test content 2" ) httpx_mock.add_response( url="https://test_url?param3=test", method="PUT", content=b"test content 3" ) httpx_mock.add_response( url="https://test_url?param4=test", method="DELETE", content=b"test content 4" ) httpx_mock.add_response( url="https://test_url?param5=test", method="PATCH", content=b"test content 5" ) httpx_mock.add_response( url="https://test_url?param6=test", method="HEAD", content=b"test content 6" ) async with httpx.AsyncClient() as client: response = await client.post( httpx.URL("https://test_url", params={"param2": "test"}) ) assert response.content == b"test content 2" response = await client.get( httpx.URL("https://test_url", params={"param1": "test"}) ) assert response.content == b"test content 1" response = await client.put( httpx.URL("https://test_url", params={"param3": "test"}) ) assert response.content == b"test content 3" response = await client.head( httpx.URL("https://test_url", params={"param6": "test"}) ) assert response.content == b"test content 6" response = await client.patch( httpx.URL("https://test_url", params={"param5": "test"}) ) assert response.content == b"test content 5" response = await client.delete( httpx.URL("https://test_url", params={"param4": "test"}) ) assert response.content == b"test content 4" @pytest.mark.asyncio async def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url=re.compile(".*test.*")) httpx_mock.add_response(url="https://unmatched", content=b"test content") async with httpx.AsyncClient() as client: response = await client.get("https://unmatched") assert response.content == b"test content" response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio async def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://unmatched") async with httpx.AsyncClient() as client: await client.get("https://unmatched") await client.get("https://test_url", headers={"X-Test": "1"}) assert httpx_mock.get_request(url=re.compile(".*test.*")).headers["x-test"] == "1" @pytest.mark.asyncio async def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://tests_url") httpx_mock.add_response(url="https://unmatched") async with httpx.AsyncClient() as client: await client.get("https://tests_url", headers={"X-Test": "1"}) await client.get("https://unmatched", headers={"X-Test": "2"}) await client.get("https://test_url") requests = httpx_mock.get_requests(url=re.compile(".*test.*")) assert len(requests) == 2 assert requests[0].headers["x-test"] == "1" assert "x-test" not in requests[1].headers @pytest.mark.asyncio async def test_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"url": str(request.url)}) def custom_response2(request: httpx.Request) -> httpx.Response: return httpx.Response( status_code=200, extensions={"http_version": b"HTTP/2.0"}, json={"url": str(request.url)}, ) httpx_mock.add_callback(custom_response, url=re.compile(".*test.*")) httpx_mock.add_callback(custom_response2, url="https://unmatched") async with httpx.AsyncClient() as client: response = await client.get("https://unmatched") assert response.http_version == "HTTP/2.0" response = await client.get("https://test_url") assert response.http_version == "HTTP/1.1" @pytest.mark.asyncio async def test_async_callback_with_await_statement(httpx_mock: HTTPXMock) -> None: async def simulate_network_latency(request: httpx.Request): await asyncio.sleep(1) return httpx.Response( status_code=200, json={"url": str(request.url), "time": time.time()}, ) def instant_response(request: httpx.Request) -> httpx.Response: return httpx.Response( status_code=200, json={"url": str(request.url), "time": time.time()} ) httpx_mock.add_callback(simulate_network_latency) httpx_mock.add_callback(instant_response) httpx_mock.add_response(json={"url": "not a callback"}) async with httpx.AsyncClient() as client: responses = await asyncio.gather( client.get("https://slow"), client.get("https://fast_with_callback"), client.get("https://fast_with_response"), ) slow_response = responses[0].json() assert slow_response["url"] == "https://slow" fast_callback_response = responses[1].json() assert fast_callback_response["url"] == "https://fast_with_callback" fast_response = responses[2].json() assert fast_response["url"] == "not a callback" # Ensure slow request was properly awaited (did not block subsequent async queries) assert math.isclose(slow_response["time"], fast_callback_response["time"] + 1) @pytest.mark.asyncio async def test_async_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"url": str(request.url)}) async def custom_response2(request: httpx.Request) -> httpx.Response: return httpx.Response( status_code=200, extensions={"http_version": b"HTTP/2.0"}, json={"url": str(request.url)}, ) httpx_mock.add_callback(custom_response, url=re.compile(".*test.*")) httpx_mock.add_callback(custom_response2, url="https://unmatched") async with httpx.AsyncClient() as client: response = await client.get("https://unmatched") assert response.http_version == "HTTP/2.0" response = await client.get("https://test_url") assert response.http_version == "HTTP/1.1" @pytest.mark.asyncio async def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param1": "test"}), method="GET", content=b"test content 1", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param2": "test"}), method="POST", content=b"test content 2", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param3": "test"}), method="PUT", content=b"test content 3", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param4": "test"}), method="DELETE", content=b"test content 4", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param5": "test"}), method="PATCH", content=b"test content 5", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param6": "test"}), method="HEAD", content=b"test content 6", ) async with httpx.AsyncClient() as client: response = await client.post("https://test_url?param2=test") assert response.content == b"test content 2" response = await client.get("https://test_url?param1=test") assert response.content == b"test content 1" response = await client.put("https://test_url?param3=test") assert response.content == b"test content 3" response = await client.head("https://test_url?param6=test") assert response.content == b"test content 6" response = await client.patch("https://test_url?param5=test") assert response.content == b"test content 5" response = await client.delete("https://test_url?param4=test") assert response.content == b"test content 4" @pytest.mark.asyncio async def test_with_http_version_2(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", http_version="HTTP/2", content=b"test content 1" ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" assert response.http_version == "HTTP/2" @pytest.mark.asyncio async def test_with_headers(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content 1", headers={"X-Test": "Test value"}, ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"test content 1" assert response.headers == httpx.Headers( {"x-test": "Test value", "content-length": "14"} ) @pytest.mark.asyncio async def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) httpx_mock.add_response( url="https://test_url", method="POST", content=b"test content 2" ) httpx_mock.add_response( url="https://test_url", method="PUT", content=b"test content 3" ) httpx_mock.add_response( url="https://test_url", method="DELETE", content=b"test content 4" ) httpx_mock.add_response( url="https://test_url", method="PATCH", content=b"test content 5" ) httpx_mock.add_response( url="https://test_url", method="HEAD", content=b"test content 6" ) async with httpx.AsyncClient() as client: await client.post("https://test_url", content=b"sent content 2") await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.put("https://test_url", content=b"sent content 3") await client.head("https://test_url") await client.patch("https://test_url", content=b"sent content 5") await client.delete("https://test_url", headers={"X-Test": "test header 4"}) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PATCH").read() == b"sent content 5" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="HEAD").read() == b"" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PUT").read() == b"sent content 3" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="GET").headers[ "x-test" ] == "test header 1" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="POST").read() == b"sent content 2" ) assert ( httpx_mock.get_request( url=httpx.URL("https://test_url"), method="DELETE" ).headers["x-test"] == "test header 4" ) @pytest.mark.asyncio async def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url", headers={"X-TEST": "test header 2"}) requests = httpx_mock.get_requests(url=httpx.URL("https://test_url")) assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" @pytest.mark.asyncio async def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url2", headers={"X-TEST": "test header 2"}) request = httpx_mock.get_request(url=httpx.URL("https://test_url")) assert request.headers["x-test"] == "test header 1" @pytest.mark.asyncio async def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url2", headers={"X-TEST": "test header 2"}) requests = httpx_mock.get_requests(method="GET") assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" @pytest.mark.asyncio async def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.post("https://test_url", headers={"X-TEST": "test header 2"}) request = httpx_mock.get_request(method="GET") assert request.headers["x-test"] == "test header 1" @pytest.mark.asyncio async def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url", headers={"X-TEST": "test header 2"}) await client.post("https://test_url", headers={"X-TEST": "test header 3"}) await client.get("https://test_url2", headers={"X-TEST": "test header 4"}) requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"), method="GET") assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" @pytest.mark.asyncio async def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.post("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url2", headers={"X-TEST": "test header 2"}) requests = httpx_mock.get_requests() assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" @pytest.mark.asyncio async def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() async with httpx.AsyncClient() as client: await client.post("https://test_url", headers={"X-TEST": "test header 1"}) request = httpx_mock.get_request() assert request.headers["x-test"] == "test header 1" @pytest.mark.asyncio async def test_requests_json_body(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", json=["list content 1", "list content 2"] ) httpx_mock.add_response( url="https://test_url", method="POST", json={"key 1": "value 1", "key 2": "value 2"}, ) httpx_mock.add_response(url="https://test_url", method="PUT", json="string value") async with httpx.AsyncClient() as client: response = await client.post("https://test_url") assert response.json() == {"key 1": "value 1", "key 2": "value 2"} assert response.headers["content-type"] == "application/json" response = await client.get("https://test_url") assert response.json() == ["list content 1", "list content 2"] assert response.headers["content-type"] == "application/json" response = await client.put("https://test_url") assert response.json() == "string value" assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_callback_raising_exception(httpx_mock: HTTPXMock) -> None: def raise_timeout(request: httpx.Request) -> httpx.Response: raise httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']['read']}", request=request, ) httpx_mock.add_callback(raise_timeout, url="https://test_url") async with httpx.AsyncClient() as client: with pytest.raises(httpx.ReadTimeout) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" @pytest.mark.asyncio async def test_async_callback_raising_exception(httpx_mock: HTTPXMock) -> None: async def raise_timeout(request: httpx.Request) -> httpx.Response: raise httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']['read']}", request=request, ) httpx_mock.add_callback(raise_timeout, url="https://test_url") async with httpx.AsyncClient() as client: with pytest.raises(httpx.ReadTimeout) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" @pytest.mark.asyncio async def test_request_exception_raising(httpx_mock: HTTPXMock) -> None: httpx_mock.add_exception( httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url" ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.ReadTimeout) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" assert exception_info.value.request is not None @pytest.mark.asyncio async def test_non_request_exception_raising(httpx_mock: HTTPXMock) -> None: httpx_mock.add_exception( httpx.HTTPError("Unable to read within 5.0"), url="https://test_url" ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.HTTPError) as exception_info: await client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" @pytest.mark.asyncio async def test_callback_returning_response(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"url": str(request.url)}) httpx_mock.add_callback(custom_response, url="https://test_url") async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == {"url": "https://test_url"} assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_async_callback_returning_response(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"url": str(request.url)}) httpx_mock.add_callback(custom_response, url="https://test_url") async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == {"url": "https://test_url"} assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" response = await client.post("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_async_callback_executed_twice(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" response = await client.post("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content2"]) httpx_mock.add_response(json=["content1"]) httpx_mock.add_callback(custom_response, is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" # Assert that the last registered callback is sent again even if there is a response response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_async_callback_registered_after_response(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content2"]) httpx_mock.add_response(json=["content1"]) httpx_mock.add_callback(custom_response, is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" # Assert that the last registered callback is sent again even if there is a response response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content1"]) httpx_mock.add_callback(custom_response) httpx_mock.add_response(json=["content2"], is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" # Assert that the last registered response is sent again even if there is a callback response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_response_registered_after_async_callback(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content1"]) httpx_mock.add_callback(custom_response) httpx_mock.add_response(json=["content2"], is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" # Assert that the last registered response is sent again even if there is a callback response = await client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_callback_matching_method(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, method="GET", is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" response = await client.get("https://test_url2") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" @pytest.mark.asyncio async def test_async_callback_matching_method(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, method="GET", is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" response = await client.get("https://test_url2") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None: """ Single request cannot be returned if there is more than one matching. """ testdir.makepyfile( """ import httpx import pytest @pytest.mark.asyncio async def test_request_retrieval_with_more_than_one(httpx_mock): httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", headers={"X-TEST": "test header 1"}) await client.get("https://test_url", headers={"X-TEST": "test header 2"}) httpx_mock.get_request(url=httpx.URL("https://test_url")) """ ) result = testdir.runpytest() result.assert_outcomes(failed=1) result.stdout.fnmatch_lines( [ "*AssertionError: More than one request (2) matched, use get_requests instead or refine your filters." ] ) @pytest.mark.asyncio async def test_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"} ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio async def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"}) async with httpx.AsyncClient() as client: response = await client.get( "https://test_url", headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")], ) assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1"}, is_optional=True ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get( "https://test_url", headers=[ ("my-custom-header", "value1"), ("my-custom-header", "value2"), ], ) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst: - Match any request with {'my-custom-header': 'value1'} headers""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1, value2"}, is_optional=True ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get( "https://test_url", headers=[ ("my-custom-header", "value1"), ("my-custom-header", "value3"), ], ) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst: - Match any request with {'my-custom-header': 'value1, value2'} headers""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}, is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("https://test_url") assert ( str(exception_info.value) == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: - Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", "Host2": "test_url", }, is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("https://test_url") assert ( str(exception_info.value) == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_not_matching_upper_case_headers_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( method="GET", url="https://test_url?q=b", match_headers={"MyHeader": "Something"}, is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("https://test_url", headers={"MyHeader": "Something"}) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst: - Match GET request on https://test_url?q=b with {'MyHeader': 'Something'} headers""" ) @pytest.mark.asyncio async def test_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") async with httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.read() == b"" @pytest.mark.asyncio async def test_proxy_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/") async with httpx.AsyncClient(proxy="http://user:pwd@my_other_proxy") as client: response = await client.get("https://test_url") assert response.read() == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) async with httpx.AsyncClient(proxy="http://my_test_proxy") as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("http://test_url") assert ( str(exception_info.value) == """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst: - Match any request with http://my_test_proxy proxy URL""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("http://test_url") assert ( str(exception_info.value) == """No response can be found for GET request on http://test_url with no proxy URL amongst: - Match any request with http://my_test_proxy proxy URL""" ) @pytest.mark.asyncio async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.post("https://test_url", content=b"This is the body") await client.post("https://test_url2", content=b"This is the body") await client.post("https://test_url2", content=b"This is the body2") assert len(httpx_mock.get_requests(match_content=b"This is the body")) == 2 @pytest.mark.asyncio async def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.post("https://test_url", json=["my_str"]) await client.post("https://test_url2", json=["my_str"]) await client.post("https://test_url2", json=["my_str2"]) assert len(httpx_mock.get_requests(match_json=["my_str"])) == 2 @pytest.mark.asyncio async def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient( mounts={ "http://": httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"), "https://": httpx.AsyncHTTPTransport( proxy="http://user:pwd@my_other_proxy" ), } ) as client: await client.get("https://test_url") await client.get("https://test_url2") await client.get("http://test_url2") assert ( len(httpx_mock.get_requests(proxy_url="http://user:pwd@my_other_proxy/")) == 2 ) @pytest.mark.asyncio async def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient( mounts={ "http://": httpx.AsyncHTTPTransport(proxy="http://my_test_proxy"), "https://": httpx.AsyncHTTPTransport( proxy="http://user:pwd@my_other_proxy" ), } ) as client: await client.get("https://test_url") await client.get("https://test_url2") await client.get("http://test_url2") assert httpx_mock.get_request(proxy_url="http://my_test_proxy/") @pytest.mark.asyncio async def test_requests_retrieval_files_and_data_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.put( "https://test_url", files={"name": ("file_name", b"File content")}, data={"field1": "value"}, ) await client.put( "https://test_url2", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) await client.put( "http://test_url2", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) assert ( len( httpx_mock.get_requests( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) ) == 2 ) @pytest.mark.asyncio async def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.put( "https://test_url", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) await client.get("https://test_url2") await client.get("http://test_url2") assert httpx_mock.get_request( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) @pytest.mark.asyncio async def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url") await client.get("https://test_url2", timeout=10) await client.get("https://test_url2", timeout=10) assert ( len( httpx_mock.get_requests( match_extensions={ "timeout": {"connect": 10, "read": 10, "write": 10, "pool": 10} } ) ) == 2 ) @pytest.mark.asyncio async def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) async with httpx.AsyncClient() as client: await client.get("https://test_url", timeout=httpx.Timeout(5, read=10)) await client.get("https://test_url2", timeout=10) await client.get("http://test_url2", timeout=10) assert httpx_mock.get_request( match_extensions={"timeout": {"connect": 5, "read": 10, "write": 5, "pool": 5}} ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body", is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body2") assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with b'This is the body2' body amongst: - Match any request with b'This is the body' body""" ) @pytest.mark.asyncio async def test_json_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) async with httpx.AsyncClient() as client: response = await client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" @pytest.mark.asyncio async def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": ANY}) async with httpx.AsyncClient() as client: response = await client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with b'{"c":3,"b":2,"a":1}' body amongst: - Match any request with {'a': 1, 'b': 2} json body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, match_headers={"foo": "bar"}, is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with {} headers and b'{"c":3,"b":2,"a":1}' body amongst: - Match any request with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"foobar") assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with b'foobar' body amongst: - Match any request with {'a': 1, 'b': 2} json body""" ) @pytest.mark.asyncio async def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, match_content=b"This is the body", ) async with httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio async def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, match_content=b"This is the body", ) async with httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio async def test_method_and_url_and_headers_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, match_content=b"This is the body", ) async with httpx.AsyncClient() as client: response = await client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="PUT", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.asyncio async def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")] ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} @pytest.mark.asyncio async def test_header_as_bytes_tuple_list(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( headers=[(b"set-cookie", b"key=value"), (b"set-cookie", b"key2=value2")] ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} @pytest.mark.asyncio async def test_header_as_bytes_dict(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(headers={b"set-cookie": b"key=value"}) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} @pytest.mark.asyncio async def test_header_as_httpx_headers(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(headers=httpx.Headers({"set-cookie": "key=value"})) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} @pytest.mark.asyncio async def test_elapsed_when_add_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.elapsed is not None @pytest.mark.asyncio async def test_elapsed_when_add_callback(httpx_mock: HTTPXMock) -> None: httpx_mock.add_callback( callback=lambda req: httpx.Response(status_code=200, json={"foo": "bar"}) ) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.elapsed is not None @pytest.mark.asyncio async def test_elapsed_when_add_async_callback(httpx_mock: HTTPXMock) -> None: async def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"foo": "bar"}) httpx_mock.add_callback(custom_response) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.elapsed is not None @pytest.mark.asyncio async def test_non_ascii_url_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?query_type=数据") async with httpx.AsyncClient() as client: response = await client.get("https://test_url?query_type=数据") assert response.content == b"" @pytest.mark.asyncio async def test_url_encoded_matching_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=%E6%95%B0%E6%8D%AE") async with httpx.AsyncClient() as client: response = await client.get("https://test_url?a=数据") assert response.content == b"" @pytest.mark.asyncio async def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() async with httpx.AsyncClient() as client: await client.get("https://test_url") assert len(httpx_mock.get_requests()) == 1 httpx_mock.reset() assert len(httpx_mock.get_requests()) == 0 @pytest.mark.asyncio async def test_mutating_json(httpx_mock: HTTPXMock) -> None: mutating_json = {"content": "request 1"} httpx_mock.add_response(json=mutating_json) mutating_json["content"] = "request 2" httpx_mock.add_response(json=mutating_json) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.json() == {"content": "request 1"} response = await client.get("https://test_url") assert response.json() == {"content": "request 2"} @pytest.mark.asyncio async def test_streams_are_not_cascading_resulting_in_maximum_recursion( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(json={"abc": "def"}, is_reusable=True) async with httpx.AsyncClient() as client: tasks = [client.get("https://test_url") for _ in range(950)] await asyncio.gather(*tasks) # No need to assert anything, this test case ensure that no error was raised by the gather @pytest.mark.asyncio async def test_custom_transport(httpx_mock: HTTPXMock) -> None: class CustomTransport(httpx.AsyncHTTPTransport): def __init__(self, prefix: str, *args, **kwargs): super().__init__(*args, **kwargs) self.prefix = prefix async def handle_async_request( self, request: httpx.Request, ) -> httpx.Response: httpx_response = await super().handle_async_request(request) httpx_response.headers["x-prefix"] = self.prefix return httpx_response httpx_mock.add_response() async with httpx.AsyncClient(transport=CustomTransport(prefix="test")) as client: response = await client.post("https://test_url", content=b"This is the body") assert response.read() == b"" assert response.headers["x-prefix"] == "test" @pytest.mark.asyncio async def test_response_selection_content_matching_with_async_iterable( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1") httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2") async def stream_content_1() -> AsyncIterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 1" async def stream_content_2() -> AsyncIterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 2" async with httpx.AsyncClient() as client: response_2 = await client.put("https://test_url", content=stream_content_2()) response_1 = await client.put("https://test_url", content=stream_content_1()) assert response_1.content == b"matched 1" assert response_2.content == b"matched 2" @pytest.mark.asyncio async def test_request_selection_content_matching_with_async_iterable( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(match_content=b"full content 1") httpx_mock.add_response(match_content=b"full content 2") async def stream_content_1() -> AsyncIterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 1" async def stream_content_2() -> AsyncIterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 2" async with httpx.AsyncClient() as client: await client.put("https://test_url_2", content=stream_content_2()) await client.put("https://test_url_1", content=stream_content_1()) assert ( httpx_mock.get_request(match_content=b"full content 1").url == "https://test_url_1" ) assert ( httpx_mock.get_request(match_content=b"full content 2").url == "https://test_url_2" ) @pytest.mark.asyncio async def test_files_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_files={"name": ("file_name", b"File content")}) async with httpx.AsyncClient() as client: response = await client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) assert response.content == b"" @pytest.mark.asyncio async def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) async with httpx.AsyncClient() as client: response = await client.put( "https://test_url", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name2": ("file_name", b"File content")}, is_optional=True ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name1": ("file_name", b"File content")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name1"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'name2': ('file_name', b'File content')} files""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name": ("file_name2", b"File content")}, is_optional=True ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name": ("file_name1", b"File content")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name1"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'name': ('file_name2', b'File content')} files""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name": ("file_name", b"File content2")}, is_optional=True ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name": ("file_name", b"File content1")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content1\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'name': ('file_name', b'File content2')} files""" ) @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_files_matching_but_data_not_matching( httpx_mock: HTTPXMock, monkeypatch ) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'field': 'value'} multipart data and {'name': ('file_name', b'File content')} files""" ) @pytest.mark.asyncio async def test_timeout_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}} ) async with httpx.AsyncClient() as client: response = await client.put( "https://test_url", timeout=httpx.Timeout(5, write=10) ) assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}}, is_optional=True, ) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("https://test_url", extensions={"test": "value"}) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'timeout': {'connect': 5.0, 'read': 5.0, 'write': 5.0, 'pool': 5.0}} extensions amongst: - Match any request with {'timeout': {'connect': 5, 'read': 5, 'write': 10, 'pool': 5}} extensions""" ) @pytest.mark.asyncio async def test_extensions_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_extensions={"test": "value"}) async with httpx.AsyncClient() as client: response = await client.put( "https://test_url", extensions={"test": "value", "test2": "value2"} ) assert response.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) async def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_extensions={"test": "value"}, is_optional=True) async with httpx.AsyncClient() as client: with pytest.raises(httpx.TimeoutException) as exception_info: await client.get("https://test_url", extensions={"test": "value2"}) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'test': 'value2'} extensions amongst: - Match any request with {'test': 'value'} extensions""" ) @pytest.mark.asyncio async def test_optional_response_not_matched(httpx_mock: HTTPXMock) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") async with httpx.AsyncClient() as client: response = await client.get("https://test_url2") assert response.content == b"" @pytest.mark.asyncio async def test_optional_response_matched(httpx_mock: HTTPXMock) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") async with httpx.AsyncClient() as client: response1 = await client.get("https://test_url") response2 = await client.get("https://test_url2") assert response1.content == b"" assert response2.content == b"" @pytest.mark.asyncio @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) async def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url") # This response MUST be requested (overrides global settings via marker) httpx_mock.add_response(url="https://test_url2", is_optional=False) async with httpx.AsyncClient() as client: response = await client.get("https://test_url2") assert response.content == b"" @pytest.mark.asyncio async def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) async with httpx.AsyncClient() as client: response = await client.get("https://test_url") assert response.content == b"" @pytest.mark.asyncio async def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) async with httpx.AsyncClient() as client: response1 = await client.get("https://test_url") response2 = await client.get("https://test_url") assert response1.content == b"" assert response2.content == b"" pytest_httpx-0.35.0/tests/test_httpx_sync.py000066400000000000000000002403241472214060000213350ustar00rootroot00000000000000import os import re from collections.abc import Iterable from unittest.mock import ANY import httpx import pytest from pytest import Testdir import pytest_httpx from pytest_httpx import HTTPXMock @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_without_response(httpx_mock: HTTPXMock) -> None: with pytest.raises(Exception) as exception_info: with httpx.Client() as client: client.get("https://test_url") assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url""" ) def test_default_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" assert response.status_code == 200 assert response.headers == httpx.Headers({}) assert response.http_version == "HTTP/1.1" def test_url_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" def test_url_matching_reusing_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" response = client.post("https://test_url") assert response.content == b"" def test_url_query_string_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&b=2", is_reusable=True) with httpx.Client() as client: response = client.post("https://test_url?a=1&b=2") assert response.content == b"" # Parameters order should not matter response = client.get("https://test_url?b=2&a=1") assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("https://test_url2") assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url2 amongst: - Match any request on https://test_url""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2", is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: # Same parameter order matters as it corresponds to a list on server side client.get("https://test_url?a=2&a=1") assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url?a=2&a=1 amongst: - Match any request on https://test_url?a=1&a=2""" ) def test_method_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get", is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" response = client.get("https://test_url2") assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get", is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url") assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url amongst: - Match GET request""" ) def test_reusing_one_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content", is_reusable=True ) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content" response = client.get("https://test_url") assert response.content == b"test content" def test_response_with_string_body(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", text="test content") with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content" def test_response_with_html_string_body(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", html="test content") with httpx.Client() as client: response = client.get("https://test_url") assert response.text == "test content" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_not_matching_upper_case_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( method="GET", url="https://test_url?q=b", match_headers={"MyHeader": "Something"}, is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("https://test_url", headers={"MyHeader": "Something"}) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'MyHeader': 'Something'} headers amongst: - Match GET request on https://test_url?q=b with {'MyHeader': 'Something'} headers""" ) def test_stream_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", stream=pytest_httpx.IteratorStream([b"part 1", b"part 2"]), is_reusable=True, ) with httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1", b"part 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1", b"part 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) def test_content_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", content=b"part 1 and 2", is_reusable=True, ) with httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) def test_text_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", text="part 1 and 2", is_reusable=True, ) with httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [b"part 1 and 2"] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) def test_default_response_streaming(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) # Assert a response can be streamed more than once with client.stream(method="GET", url="https://test_url") as response: assert list(response.iter_raw()) == [] # Assert that stream still behaves the proper way (can only be consumed once per request) with pytest.raises(httpx.StreamConsumed): list(response.iter_raw()) def test_with_many_responses(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response(url="https://test_url", content=b"test content 2") httpx_mock.add_response(url="https://test_url", content=b"test content 2") with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" response = client.get("https://test_url") assert response.content == b"test content 2" response = client.get("https://test_url") assert response.content == b"test content 2" def test_with_many_reused_responses(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content 1") httpx_mock.add_response( url="https://test_url", content=b"test content 2", is_reusable=True ) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" response = client.get("https://test_url") assert response.content == b"test content 2" response = client.get("https://test_url") assert response.content == b"test content 2" def test_with_many_responses_methods(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) httpx_mock.add_response( url="https://test_url", method="POST", content=b"test content 2" ) httpx_mock.add_response( url="https://test_url", method="PUT", content=b"test content 3" ) httpx_mock.add_response( url="https://test_url", method="DELETE", content=b"test content 4" ) httpx_mock.add_response( url="https://test_url", method="PATCH", content=b"test content 5" ) httpx_mock.add_response( url="https://test_url", method="HEAD", content=b"test content 6" ) with httpx.Client() as client: response = client.post("https://test_url") assert response.content == b"test content 2" response = client.get("https://test_url") assert response.content == b"test content 1" response = client.put("https://test_url") assert response.content == b"test content 3" response = client.head("https://test_url") assert response.content == b"test content 6" response = client.patch("https://test_url") assert response.content == b"test content 5" response = client.delete("https://test_url") assert response.content == b"test content 4" def test_with_many_responses_status_codes(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1", status_code=200 ) httpx_mock.add_response( url="https://test_url", method="POST", content=b"test content 2", status_code=201, ) httpx_mock.add_response( url="https://test_url", method="PUT", content=b"test content 3", status_code=202 ) httpx_mock.add_response( url="https://test_url", method="DELETE", content=b"test content 4", status_code=303, ) httpx_mock.add_response( url="https://test_url", method="PATCH", content=b"test content 5", status_code=404, ) httpx_mock.add_response( url="https://test_url", method="HEAD", content=b"test content 6", status_code=500, ) with httpx.Client() as client: response = client.post("https://test_url") assert response.content == b"test content 2" assert response.status_code == 201 response = client.get("https://test_url") assert response.content == b"test content 1" assert response.status_code == 200 response = client.put("https://test_url") assert response.content == b"test content 3" assert response.status_code == 202 response = client.head("https://test_url") assert response.content == b"test content 6" assert response.status_code == 500 response = client.patch("https://test_url") assert response.content == b"test content 5" assert response.status_code == 404 response = client.delete("https://test_url") assert response.content == b"test content 4" assert response.status_code == 303 def test_with_many_responses_urls_str(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url?param1=test", method="GET", content=b"test content 1" ) httpx_mock.add_response( url="https://test_url?param2=test", method="POST", content=b"test content 2" ) httpx_mock.add_response( url="https://test_url?param3=test", method="PUT", content=b"test content 3" ) httpx_mock.add_response( url="https://test_url?param4=test", method="DELETE", content=b"test content 4" ) httpx_mock.add_response( url="https://test_url?param5=test", method="PATCH", content=b"test content 5" ) httpx_mock.add_response( url="https://test_url?param6=test", method="HEAD", content=b"test content 6" ) with httpx.Client() as client: response = client.post(httpx.URL("https://test_url", params={"param2": "test"})) assert response.content == b"test content 2" response = client.get(httpx.URL("https://test_url", params={"param1": "test"})) assert response.content == b"test content 1" response = client.put(httpx.URL("https://test_url", params={"param3": "test"})) assert response.content == b"test content 3" response = client.head(httpx.URL("https://test_url", params={"param6": "test"})) assert response.content == b"test content 6" response = client.patch( httpx.URL("https://test_url", params={"param5": "test"}) ) assert response.content == b"test content 5" response = client.delete( httpx.URL("https://test_url", params={"param4": "test"}) ) assert response.content == b"test content 4" def test_response_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url=re.compile(".*test.*")) httpx_mock.add_response(url="https://unmatched", content=b"test content") with httpx.Client() as client: response = client.get("https://unmatched") assert response.content == b"test content" response = client.get("https://test_url") assert response.content == b"" def test_request_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://unmatched") with httpx.Client() as client: client.get("https://unmatched") client.get("https://test_url", headers={"X-Test": "1"}) assert httpx_mock.get_request(url=re.compile(".*test.*")).headers["x-test"] == "1" def test_requests_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") httpx_mock.add_response(url="https://tests_url") httpx_mock.add_response(url="https://unmatched") with httpx.Client() as client: client.get("https://tests_url", headers={"X-Test": "1"}) client.get("https://unmatched", headers={"X-Test": "2"}) client.get("https://test_url") requests = httpx_mock.get_requests(url=re.compile(".*test.*")) assert len(requests) == 2 assert requests[0].headers["x-test"] == "1" assert "x-test" not in requests[1].headers def test_callback_with_pattern_in_url(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"url": str(request.url)}) def custom_response2(request: httpx.Request) -> httpx.Response: return httpx.Response( status_code=200, extensions={"http_version": b"HTTP/2.0"}, json={"url": str(request.url)}, ) httpx_mock.add_callback(custom_response, url=re.compile(".*test.*")) httpx_mock.add_callback(custom_response2, url="https://unmatched") with httpx.Client() as client: response = client.get("https://unmatched") assert response.http_version == "HTTP/2.0" response = client.get("https://test_url") assert response.http_version == "HTTP/1.1" def test_with_many_responses_urls_instances(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param1": "test"}), method="GET", content=b"test content 1", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param2": "test"}), method="POST", content=b"test content 2", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param3": "test"}), method="PUT", content=b"test content 3", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param4": "test"}), method="DELETE", content=b"test content 4", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param5": "test"}), method="PATCH", content=b"test content 5", ) httpx_mock.add_response( url=httpx.URL("https://test_url", params={"param6": "test"}), method="HEAD", content=b"test content 6", ) with httpx.Client() as client: response = client.post("https://test_url?param2=test") assert response.content == b"test content 2" response = client.get("https://test_url?param1=test") assert response.content == b"test content 1" response = client.put("https://test_url?param3=test") assert response.content == b"test content 3" response = client.head("https://test_url?param6=test") assert response.content == b"test content 6" response = client.patch("https://test_url?param5=test") assert response.content == b"test content 5" response = client.delete("https://test_url?param4=test") assert response.content == b"test content 4" def test_with_http_version_2(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", http_version="HTTP/2", content=b"test content 1" ) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" assert response.http_version == "HTTP/2" def test_with_headers(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", content=b"test content 1", headers={"X-Test": "Test value"}, ) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"test content 1" assert response.headers == httpx.Headers( {"x-test": "Test value", "content-length": "14"} ) def test_requests_retrieval(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", content=b"test content 1" ) httpx_mock.add_response( url="https://test_url", method="POST", content=b"test content 2" ) httpx_mock.add_response( url="https://test_url", method="PUT", content=b"test content 3" ) httpx_mock.add_response( url="https://test_url", method="DELETE", content=b"test content 4" ) httpx_mock.add_response( url="https://test_url", method="PATCH", content=b"test content 5" ) httpx_mock.add_response( url="https://test_url", method="HEAD", content=b"test content 6" ) with httpx.Client() as client: client.post("https://test_url", content=b"sent content 2") client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.put("https://test_url", content=b"sent content 3") client.head("https://test_url") client.patch("https://test_url", content=b"sent content 5") client.delete("https://test_url", headers={"X-Test": "test header 4"}) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PATCH").read() == b"sent content 5" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="HEAD").read() == b"" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="PUT").read() == b"sent content 3" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="GET").headers[ "x-test" ] == "test header 1" ) assert ( httpx_mock.get_request(url=httpx.URL("https://test_url"), method="POST").read() == b"sent content 2" ) assert ( httpx_mock.get_request( url=httpx.URL("https://test_url"), method="DELETE" ).headers["x-test"] == "test header 4" ) def test_requests_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) with httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url", headers={"X-TEST": "test header 2"}) requests = httpx_mock.get_requests(url=httpx.URL("https://test_url")) assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" def test_request_retrieval_on_same_url(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url2", headers={"X-TEST": "test header 2"}) request = httpx_mock.get_request(url=httpx.URL("https://test_url")) assert request.headers["x-test"] == "test header 1" def test_requests_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url2", headers={"X-TEST": "test header 2"}) requests = httpx_mock.get_requests(method="GET") assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" def test_request_retrieval_on_same_method(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.post("https://test_url", headers={"X-TEST": "test header 2"}) request = httpx_mock.get_request(method="GET") assert request.headers["x-test"] == "test header 1" def test_requests_retrieval_on_same_url_and_method(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url", headers={"X-TEST": "test header 2"}) client.post("https://test_url", headers={"X-TEST": "test header 3"}) client.get("https://test_url2", headers={"X-TEST": "test header 4"}) requests = httpx_mock.get_requests(url=httpx.URL("https://test_url"), method="GET") assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" def test_default_requests_retrieval(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.post("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url2", headers={"X-TEST": "test header 2"}) requests = httpx_mock.get_requests() assert len(requests) == 2 assert requests[0].headers["x-test"] == "test header 1" assert requests[1].headers["x-test"] == "test header 2" def test_default_request_retrieval(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() with httpx.Client() as client: client.post("https://test_url", headers={"X-TEST": "test header 1"}) request = httpx_mock.get_request() assert request.headers["x-test"] == "test header 1" def test_requests_json_body(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="GET", json=["list content 1", "list content 2"] ) httpx_mock.add_response( url="https://test_url", method="POST", json={"key 1": "value 1", "key 2": "value 2"}, ) httpx_mock.add_response(url="https://test_url", method="PUT", json="string value") with httpx.Client() as client: response = client.post("https://test_url") assert response.json() == {"key 1": "value 1", "key 2": "value 2"} assert response.headers["content-type"] == "application/json" response = client.get("https://test_url") assert response.json() == ["list content 1", "list content 2"] assert response.headers["content-type"] == "application/json" response = client.put("https://test_url") assert response.json() == "string value" assert response.headers["content-type"] == "application/json" def test_callback_raising_exception(httpx_mock: HTTPXMock) -> None: def raise_timeout(request: httpx.Request) -> httpx.Response: raise httpx.ReadTimeout( f"Unable to read within {request.extensions['timeout']['read']}", request=request, ) httpx_mock.add_callback(raise_timeout, url="https://test_url") with httpx.Client() as client: with pytest.raises(httpx.ReadTimeout) as exception_info: client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" def test_request_exception_raising(httpx_mock: HTTPXMock) -> None: httpx_mock.add_exception( httpx.ReadTimeout("Unable to read within 5.0"), url="https://test_url" ) with httpx.Client() as client: with pytest.raises(httpx.ReadTimeout) as exception_info: client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" assert exception_info.value.request is not None def test_non_request_exception_raising(httpx_mock: HTTPXMock) -> None: httpx_mock.add_exception( httpx.HTTPError("Unable to read within 5.0"), url="https://test_url" ) with httpx.Client() as client: with pytest.raises(httpx.HTTPError) as exception_info: client.get("https://test_url") assert str(exception_info.value) == "Unable to read within 5.0" def test_callback_returning_response(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json={"url": str(request.url)}) httpx_mock.add_callback(custom_response, url="https://test_url") with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == {"url": "https://test_url"} assert response.headers["content-type"] == "application/json" def test_callback_executed_twice(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" response = client.post("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" def test_callback_registered_after_response(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content2"]) httpx_mock.add_response(json=["content1"]) httpx_mock.add_callback(custom_response, is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" response = client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" # Assert that the last registered callback is sent again even if there is a response response = client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" def test_response_registered_after_callback(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content1"]) httpx_mock.add_callback(custom_response) httpx_mock.add_response(json=["content2"], is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content1"] assert response.headers["content-type"] == "application/json" response = client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" # Assert that the last registered response is sent again even if there is a callback response = client.post("https://test_url") assert response.json() == ["content2"] assert response.headers["content-type"] == "application/json" def test_callback_matching_method(httpx_mock: HTTPXMock) -> None: def custom_response(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code=200, json=["content"]) httpx_mock.add_callback(custom_response, method="GET", is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" response = client.get("https://test_url2") assert response.json() == ["content"] assert response.headers["content-type"] == "application/json" def test_request_retrieval_with_more_than_one(testdir: Testdir) -> None: """ Single request cannot be returned if there is more than one matching. """ testdir.makepyfile( """ import httpx def test_request_retrieval_with_more_than_one(httpx_mock): httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url", headers={"X-TEST": "test header 1"}) client.get("https://test_url", headers={"X-TEST": "test header 2"}) httpx_mock.get_request(url=httpx.URL("https://test_url")) """ ) result = testdir.runpytest() result.assert_outcomes(failed=1) result.stdout.fnmatch_lines( [ "*AssertionError: More than one request (2) matched, use get_requests instead or refine your filters." ] ) def test_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"} ) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" def test_multi_value_headers_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_headers={"my-custom-header": "value1, value2"}) with httpx.Client() as client: response = client.get( "https://test_url", headers=[("my-custom-header", "value1"), ("my-custom-header", "value2")], ) assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1"}, is_optional=True ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get( "https://test_url", headers=[ ("my-custom-header", "value1"), ("my-custom-header", "value2"), ], ) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value2'} headers amongst: - Match any request with {'my-custom-header': 'value1'} headers""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_multi_value_headers_not_matching_multi_value_issued( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( match_headers={"my-custom-header": "value1, value2"}, is_optional=True ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get( "https://test_url", headers=[ ("my-custom-header", "value1"), ("my-custom-header", "value3"), ], ) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'my-custom-header': 'value1, value3'} headers amongst: - Match any request with {'my-custom-header': 'value1, value2'} headers""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"user-agent": f"python-httpx/{httpx.__version__}"}, is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("https://test_url") assert ( str(exception_info.value) == f"""No response can be found for GET request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: - Match any request with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", "Host2": "test_url", }, is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("https://test_url") assert ( str(exception_info.value) == f"""No response can be found for GET request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) def test_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") with httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.read() == b"" def test_proxy_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://user:pwd@my_other_proxy/") with httpx.Client(proxy="http://user:pwd@my_other_proxy") as client: response = client.get("https://test_url") assert response.read() == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) with httpx.Client(proxy="http://my_test_proxy") as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("http://test_url") assert ( str(exception_info.value) == """No response can be found for GET request on http://test_url with http://my_test_proxy/ proxy URL amongst: - Match any request with http://my_test_proxy proxy URL""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy", is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("http://test_url") assert ( str(exception_info.value) == """No response can be found for GET request on http://test_url with no proxy URL amongst: - Match any request with http://my_test_proxy proxy URL""" ) def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.post("https://test_url", content=b"This is the body") client.post("https://test_url2", content=b"This is the body") client.post("https://test_url2", content=b"This is the body2") assert len(httpx_mock.get_requests(match_content=b"This is the body")) == 2 def test_requests_retrieval_json_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.post("https://test_url", json=["my_str"]) client.post("https://test_url2", json=["my_str"]) client.post("https://test_url2", json=["my_str2"]) assert len(httpx_mock.get_requests(match_json=["my_str"])) == 2 def test_requests_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client( mounts={ "http://": httpx.HTTPTransport(proxy="http://my_test_proxy"), "https://": httpx.HTTPTransport(proxy="http://user:pwd@my_other_proxy"), } ) as client: client.get("https://test_url") client.get("https://test_url2") client.get("http://test_url2") assert ( len(httpx_mock.get_requests(proxy_url="http://user:pwd@my_other_proxy/")) == 2 ) def test_request_retrieval_proxy_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client( mounts={ "http://": httpx.HTTPTransport(proxy="http://my_test_proxy"), "https://": httpx.HTTPTransport(proxy="http://user:pwd@my_other_proxy"), } ) as client: client.get("https://test_url") client.get("https://test_url2") client.get("http://test_url2") assert httpx_mock.get_request(proxy_url="http://my_test_proxy/") def test_requests_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.put( "https://test_url", files={"name": ("file_name", b"File content")}, data={"field1": "value"}, ) client.put( "https://test_url2", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) client.put( "http://test_url2", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) assert ( len( httpx_mock.get_requests( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) ) == 2 ) def test_request_retrieval_files_and_data_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.put( "https://test_url", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) client.get("https://test_url2") client.get("http://test_url2") assert httpx_mock.get_request( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) def test_requests_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url") client.get("https://test_url2", timeout=10) client.get("https://test_url2", timeout=10) assert ( len( httpx_mock.get_requests( match_extensions={ "timeout": {"connect": 10, "read": 10, "write": 10, "pool": 10} } ) ) == 2 ) def test_request_retrieval_extensions_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(is_reusable=True) with httpx.Client() as client: client.get("https://test_url", timeout=httpx.Timeout(5, read=10)) client.get("https://test_url2", timeout=10) client.get("http://test_url2", timeout=10) assert httpx_mock.get_request( match_extensions={"timeout": {"connect": 5, "read": 10, "write": 5, "pool": 5}} ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body", is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body2") assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with b'This is the body2' body amongst: - Match any request with b'This is the body' body""" ) def test_match_json_and_match_content_error(httpx_mock: HTTPXMock) -> None: with pytest.raises(ValueError) as exception_info: httpx_mock.add_response(match_json={"a": 1}, match_content=b"") assert ( str(exception_info.value) == "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. If you want to match against the multipart representation, use match_files (and match_data). Otherwise, use match_content." ) def test_match_json_and_match_files_error(httpx_mock: HTTPXMock) -> None: with pytest.raises(ValueError) as exception_info: httpx_mock.add_response( match_json={"a": 1}, match_files={"name": ("file_name", b"File content")} ) assert ( str(exception_info.value) == "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. If you want to match against the multipart representation, use match_files (and match_data). Otherwise, use match_content." ) def test_match_content_and_match_files_error(httpx_mock: HTTPXMock) -> None: with pytest.raises(ValueError) as exception_info: httpx_mock.add_response( match_content=b"", match_files={"name": ("file_name", b"File content")}, ) assert ( str(exception_info.value) == "Only one way of matching against the body can be provided. If you want to match against the JSON decoded representation, use match_json. If you want to match against the multipart representation, use match_files (and match_data). Otherwise, use match_content." ) def test_json_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) with httpx.Client() as client: response = client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" def test_json_partial_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": ANY}) with httpx.Client() as client: response = client.post("https://test_url", json={"b": 2, "a": 1}) assert response.read() == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with b'{"c":3,"b":2,"a":1}' body amongst: - Match any request with {'a': 1, 'b': 2} json body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_and_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_json={"a": 1, "b": 2}, match_headers={"foo": "bar"}, is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", json={"c": 3, "b": 2, "a": 1}) assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with {} headers and b'{"c":3,"b":2,"a":1}' body amongst: - Match any request with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}, is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"foobar") assert ( str(exception_info.value) == """No response can be found for POST request on https://test_url with b'foobar' body amongst: - Match any request with {'a': 1, 'b': 2} json body""" ) def test_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, match_content=b"This is the body", ) with httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_not_matching_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_matching_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) def test_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, match_content=b"This is the body", ) with httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_not_matching_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_not_matching_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_and_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url2", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match any request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) def test_method_and_url_and_headers_and_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={"User-Agent": f"python-httpx/{httpx.__version__}"}, match_content=b"This is the body", ) with httpx.Client() as client: response = client.post("https://test_url", content=b"This is the body") assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_headers_not_matching_and_method_and_url_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_url_and_headers_not_matching_and_method_and_content_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_url_and_headers_matching_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_headers_matching_and_url_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_url_matching_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_matching_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="POST", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match POST request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_method_and_url_and_headers_and_content_not_matching( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response( url="https://test_url2", method="PUT", match_headers={ "User-Agent": f"python-httpx/{httpx.__version__}", "Host": "test_url2", }, match_content=b"This is the body2", is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.post("https://test_url", content=b"This is the body") assert ( str(exception_info.value) == f"""No response can be found for POST request on https://test_url with {{'Host': 'test_url', 'User-Agent': 'python-httpx/{httpx.__version__}'}} headers and b'This is the body' body amongst: - Match PUT request on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) def test_header_as_str_tuple_list(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( headers=[("set-cookie", "key=value"), ("set-cookie", "key2=value2")] ) with httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} def test_header_as_bytes_tuple_list(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( headers=[(b"set-cookie", b"key=value"), (b"set-cookie", b"key2=value2")] ) with httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value", "key2": "value2"} def test_header_as_bytes_dict(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(headers={b"set-cookie": b"key=value"}) with httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} def test_header_as_httpx_headers(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(headers=httpx.Headers({"set-cookie": "key=value"})) with httpx.Client() as client: response = client.get("https://test_url") assert dict(response.cookies) == {"key": "value"} def test_elapsed_when_add_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() with httpx.Client() as client: response = client.get("https://test_url") assert response.elapsed is not None def test_elapsed_when_add_callback(httpx_mock: HTTPXMock) -> None: httpx_mock.add_callback( callback=lambda req: httpx.Response(status_code=200, json={"foo": "bar"}) ) with httpx.Client() as client: response = client.get("https://test_url") assert response.elapsed is not None def test_non_ascii_url_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?query_type=数据") with httpx.Client() as client: response = client.get("https://test_url?query_type=数据") assert response.content == b"" def test_url_encoded_matching_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=%E6%95%B0%E6%8D%AE") with httpx.Client() as client: response = client.get("https://test_url?a=数据") assert response.content == b"" def test_reset_is_removing_requests(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() with httpx.Client() as client: client.get("https://test_url") assert len(httpx_mock.get_requests()) == 1 httpx_mock.reset() assert len(httpx_mock.get_requests()) == 0 def test_mutating_json(httpx_mock: HTTPXMock) -> None: mutating_json = {"content": "request 1"} httpx_mock.add_response(json=mutating_json) mutating_json["content"] = "request 2" httpx_mock.add_response(json=mutating_json) with httpx.Client() as client: response = client.get("https://test_url") assert response.json() == {"content": "request 1"} response = client.get("https://test_url") assert response.json() == {"content": "request 2"} def test_custom_transport(httpx_mock: HTTPXMock) -> None: class CustomTransport(httpx.HTTPTransport): def __init__(self, prefix: str, *args, **kwargs): super().__init__(*args, **kwargs) self.prefix = prefix def handle_request( self, request: httpx.Request, ) -> httpx.Response: httpx_response = super().handle_request(request) httpx_response.headers["x-prefix"] = self.prefix return httpx_response httpx_mock.add_response() with httpx.Client(transport=CustomTransport(prefix="test")) as client: response = client.post("https://test_url", content=b"This is the body") assert response.read() == b"" assert response.headers["x-prefix"] == "test" def test_response_selection_content_matching_with_iterable( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(match_content=b"full content 1", content=b"matched 1") httpx_mock.add_response(match_content=b"full content 2", content=b"matched 2") def stream_content_1() -> Iterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 1" def stream_content_2() -> Iterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 2" with httpx.Client() as client: response_2 = client.put("https://test_url", content=stream_content_2()) response_1 = client.put("https://test_url", content=stream_content_1()) assert response_1.content == b"matched 1" assert response_2.content == b"matched 2" def test_request_selection_content_matching_with_iterable( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(match_content=b"full content 1") httpx_mock.add_response(match_content=b"full content 2") def stream_content_1() -> Iterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 1" def stream_content_2() -> Iterable[bytes]: yield b"full" yield b" " yield b"content" yield b" 2" with httpx.Client() as client: client.put("https://test_url_2", content=stream_content_2()) client.put("https://test_url_1", content=stream_content_1()) assert ( httpx_mock.get_request(match_content=b"full content 1").url == "https://test_url_1" ) assert ( httpx_mock.get_request(match_content=b"full content 2").url == "https://test_url_2" ) def test_files_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_files={"name": ("file_name", b"File content")}) with httpx.Client() as client: response = client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) assert response.content == b"" def test_files_and_data_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, ) with httpx.Client() as client: response = client.put( "https://test_url", files={"name": ("file_name", b"File content")}, data={"field": "value"}, ) assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_files_not_matching_name(httpx_mock: HTTPXMock, monkeypatch) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name2": ("file_name", b"File content")}, is_optional=True ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name1": ("file_name", b"File content")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name1"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'name2': ('file_name', b'File content')} files""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_files_not_matching_file_name(httpx_mock: HTTPXMock, monkeypatch) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name": ("file_name2", b"File content")}, is_optional=True ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name": ("file_name1", b"File content")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name1"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'name': ('file_name2', b'File content')} files""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_files_not_matching_content(httpx_mock: HTTPXMock, monkeypatch) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name": ("file_name", b"File content2")}, is_optional=True ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name": ("file_name", b"File content1")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content1\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'name': ('file_name', b'File content2')} files""" ) @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_files_matching_but_data_not_matching( httpx_mock: HTTPXMock, monkeypatch ) -> None: # Ensure generated boundary will be fbe495efe4cd41b941ca13e254d6b018 monkeypatch.setattr( os, "urandom", lambda length: b"\xfb\xe4\x95\xef\xe4\xcdA\xb9A\xca\x13\xe2T\xd6\xb0\x18", ) httpx_mock.add_response( match_files={"name": ("file_name", b"File content")}, match_data={"field": "value"}, is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.put( "https://test_url", files={"name": ("file_name", b"File content")} ) assert ( str(exception_info.value) == """No response can be found for PUT request on https://test_url with b'--fbe495efe4cd41b941ca13e254d6b018\\r\\nContent-Disposition: form-data; name="name"; filename="file_name"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\nFile content\\r\\n--fbe495efe4cd41b941ca13e254d6b018--\\r\\n' body amongst: - Match any request with {'field': 'value'} multipart data and {'name': ('file_name', b'File content')} files""" ) def test_data_without_files(httpx_mock: HTTPXMock) -> None: with pytest.raises(ValueError) as exception_info: httpx_mock.add_response(match_data={"field": "value"}) assert ( str(exception_info.value) == "match_data is meant to be used for multipart matching (in conjunction with match_files).Use match_content to match url encoded data." ) def test_timeout_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}} ) with httpx.Client() as client: response = client.put("https://test_url", timeout=httpx.Timeout(5, write=10)) assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_timeout_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_extensions={"timeout": {"connect": 5, "read": 5, "write": 10, "pool": 5}}, is_optional=True, ) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("https://test_url", extensions={"test": "value"}) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'timeout': {'connect': 5.0, 'read': 5.0, 'write': 5.0, 'pool': 5.0}} extensions amongst: - Match any request with {'timeout': {'connect': 5, 'read': 5, 'write': 10, 'pool': 5}} extensions""" ) def test_extensions_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_extensions={"test": "value"}) with httpx.Client() as client: response = client.put( "https://test_url", extensions={"test": "value", "test2": "value2"} ) assert response.content == b"" @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_extensions_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_extensions={"test": "value"}, is_optional=True) with httpx.Client() as client: with pytest.raises(httpx.TimeoutException) as exception_info: client.get("https://test_url", extensions={"test": "value2"}) assert ( str(exception_info.value) == """No response can be found for GET request on https://test_url with {'test': 'value2'} extensions amongst: - Match any request with {'test': 'value'} extensions""" ) def test_optional_response_not_matched(httpx_mock: HTTPXMock) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") with httpx.Client() as client: response = client.get("https://test_url2") assert response.content == b"" def test_optional_response_matched(httpx_mock: HTTPXMock) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url", is_optional=True) httpx_mock.add_response(url="https://test_url2") with httpx.Client() as client: response1 = client.get("https://test_url") response2 = client.get("https://test_url2") assert response1.content == b"" assert response2.content == b"" @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_mandatory_response_matched(httpx_mock: HTTPXMock) -> None: # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url") # This response MUST be requested (overrides global settings via marker) httpx_mock.add_response(url="https://test_url2", is_optional=False) with httpx.Client() as client: response = client.get("https://test_url2") assert response.content == b"" def test_multi_response_matched_once(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) with httpx.Client() as client: response = client.get("https://test_url") assert response.content == b"" def test_multi_response_matched_twice(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", is_reusable=True) with httpx.Client() as client: response1 = client.get("https://test_url") response2 = client.get("https://test_url") assert response1.content == b"" assert response2.content == b"" pytest_httpx-0.35.0/tests/test_plugin.py000066400000000000000000000647411472214060000204370ustar00rootroot00000000000000from pytest import Testdir def test_fixture_is_available(testdir: Testdir) -> None: # create a temporary pytest test file testdir.makepyfile( """ import httpx def test_http(httpx_mock): mock = httpx_mock.add_response(url="https://foo.tld") r = httpx.get("https://foo.tld") assert httpx_mock.get_request() is not None """ ) # run all tests with pytest result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_unused_response(testdir: Testdir) -> None: """ Unused responses should fail test case. """ testdir.makepyfile( """ def test_httpx_mock_unused_response(httpx_mock): httpx_mock.add_response() """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, passed=1) result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match any request", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: """ Unused responses should not fail test case if assert_all_responses_were_requested option is set to False. """ testdir.makepyfile( """ import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_httpx_mock_unused_response_without_assertion(httpx_mock): httpx_mock.add_response() """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_unused_callback(testdir: Testdir) -> None: """ Unused callbacks should fail test case. """ testdir.makepyfile( """ def test_httpx_mock_unused_callback(httpx_mock): def unused(*args, **kwargs): pass httpx_mock.add_callback(unused) """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, passed=1) result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match any request", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: """ Unused callbacks should not fail test case if assert_all_responses_were_requested option is set to False. """ testdir.makepyfile( """ import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_httpx_mock_unused_callback_without_assertion(httpx_mock): def unused(*args, **kwargs): pass httpx_mock.add_callback(unused) """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_unexpected_request(testdir: Testdir) -> None: """ Unexpected request should fail test case if assert_all_requests_were_expected option is set to True (default). """ testdir.makepyfile( """ import httpx import pytest def test_httpx_mock_unexpected_request(httpx_mock): with httpx.Client() as client: # Non mocked request with pytest.raises(httpx.TimeoutException): client.get("https://foo.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, passed=1) result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", "* - GET request on https://foo.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request", ], consecutive=True, ) def test_httpx_mock_unexpected_request_without_assertion(testdir: Testdir) -> None: """ Unexpected request should not fail test case if assert_all_requests_were_expected option is set to False. """ testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(assert_all_requests_were_expected=False) def test_httpx_mock_unexpected_request(httpx_mock): with httpx.Client() as client: # Non mocked request with pytest.raises(httpx.TimeoutException): client.get("https://foo.tld") """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_already_matched_response(testdir: Testdir) -> None: """ Already matched response should fail test case if can_send_already_matched_responses option is set to False (default). """ testdir.makepyfile( """ import httpx import pytest def test_httpx_mock_already_matched_response(httpx_mock): httpx_mock.add_response() with httpx.Client() as client: client.get("https://foo.tld") # Non mocked (already matched) request with pytest.raises(httpx.TimeoutException): client.get("https://foo.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, passed=1) result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", "* - GET request on https://foo.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request", ], consecutive=True, ) def test_httpx_mock_reusing_matched_response(testdir: Testdir) -> None: """ Already matched response should not fail test case if can_send_already_matched_responses option is set to True. """ testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_httpx_mock_reusing_matched_response(httpx_mock): httpx_mock.add_response() with httpx.Client() as client: client.get("https://foo.tld") # Reusing response client.get("https://foo.tld") """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_unmatched_request_without_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest def test_httpx_mock_unmatched_request_without_responses(httpx_mock): with httpx.Client() as client: # This request will not be matched client.get("https://foo22.tld") # This code will not be reached client.get("https://foo3.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", "* - GET request on https://foo22.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request", ], consecutive=True, ) def test_httpx_mock_unmatched_request_with_only_unmatched_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock): # This response will not be sent (because of a typo in the URL) httpx_mock.add_response(url="https://foo2.tld") # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo3.tld") with httpx.Client() as client: # This request will not be matched client.get("https://foo22.tld") # This code will not be reached client.get("https://foo3.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", "*- Match any request on https://foo2.tld", "*- Match any request on https://foo3.tld", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match any request on https://foo2.tld", "* - Match any request on https://foo3.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_httpx_mock_unmatched_request_with_only_unmatched_reusable_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_httpx_mock_unmatched_request_with_only_unmatched_responses(httpx_mock): # This response will not be sent (because of a typo in the URL) httpx_mock.add_response(url="https://foo2.tld", method="GET") # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo3.tld") with httpx.Client() as client: # This request will not be matched client.get("https://foo22.tld") # This code will not be reached client.get("https://foo3.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", "*- Match GET request on https://foo2.tld", "*- Match every request on https://foo3.tld", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match GET request on https://foo2.tld", "* - Match every request on https://foo3.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_httpx_mock_unmatched_request_with_only_matched_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock): # Sent response httpx_mock.add_response(url="https://foo.tld") # Sent response httpx_mock.add_response(url="https://foo.tld") with httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo.tld") # This request will not be matched client.get("https://foo22.tld") # This code will not be reached client.get("https://foo3.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", "*- Already matched any request on https://foo.tld", "*- Already matched any request on https://foo.tld", "*", "*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", "* - GET request on https://foo22.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request", ], consecutive=True, ) def test_httpx_mock_unmatched_request_with_only_matched_reusable_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_httpx_mock_unmatched_request_with_only_matched_responses(httpx_mock): # Sent response httpx_mock.add_response(url="https://foo.tld") # Sent response httpx_mock.add_response(url="https://foo3.tld") with httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo.tld") client.get("https://foo3.tld") # This request will not be matched client.get("https://foo22.tld") # This code will not be reached client.get("https://foo3.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", "*- Match every request on https://foo.tld", "*- Match every request on https://foo3.tld", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following requests were not expected:", "* - GET request on https://foo22.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-not-register-responses-for-every-request", ], consecutive=True, ) def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock): # Sent response httpx_mock.add_response(url="https://foo.tld") # This response will not be sent (because of a typo in the URL) httpx_mock.add_response(url="https://foo2.tld") # Sent response httpx_mock.add_response(url="https://foo.tld") # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo3.tld") with httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo.tld") # This request will not be matched client.get("https://foo22.tld") # This code will not be reached client.get("https://foo3.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo22.tld amongst:", "*- Match any request on https://foo2.tld", "*- Match any request on https://foo3.tld", "*- Already matched any request on https://foo.tld", "*- Already matched any request on https://foo.tld", "*", "*If you wanted to reuse an already matched response instead of registering it again, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-a-response-for-more-than-one-request", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match any request on https://foo2.tld", "* - Match any request on https://foo3.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_httpx_mock_unmatched_request_with_matched_and_unmatched_reusable_responses( testdir: Testdir, ) -> None: testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(can_send_already_matched_responses=True) def test_httpx_mock_unmatched_request_with_matched_and_unmatched_responses(httpx_mock): # Sent response httpx_mock.add_response(url="https://foo.tld") # This response will not be sent (because of a typo in the URL) httpx_mock.add_response(url="https://foo33.tld") # Sent response httpx_mock.add_response(url="https://foo2.tld") # This response will not be sent (because test execution failed earlier) httpx_mock.add_response(url="https://foo4.tld") with httpx.Client() as client: client.get("https://foo.tld") client.get("https://foo2.tld") client.get("https://foo.tld") # This request will not be matched client.get("https://foo3.tld") # This code will not be reached client.get("https://foo2.tld") """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, failed=1) # Assert the error that occurred result.stdout.fnmatch_lines( [ "*httpx.TimeoutException: No response can be found for GET request on https://foo3.tld amongst:", "*- Match every request on https://foo33.tld", "*- Match every request on https://foo4.tld", "*- Match every request on https://foo.tld", "*- Match every request on https://foo2.tld", ], consecutive=True, ) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match every request on https://foo33.tld", "* - Match every request on https://foo4.tld", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_httpx_mock_should_mock_sync(testdir: Testdir) -> None: """ Non mocked requests should go through while other requests should be mocked. """ testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost") def test_httpx_mock_should_mock_sync(httpx_mock): httpx_mock.add_response() with httpx.Client() as client: # Mocked request client.get("https://foo.tld") # Non mocked request with pytest.raises(httpx.ConnectError): client.get("https://localhost:5005") # Assert that a single request was mocked assert len(httpx_mock.get_requests()) == 1 """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_should_mock_async(testdir: Testdir) -> None: """ Non mocked requests should go through while other requests should be mocked. """ testdir.makepyfile( """ import httpx import pytest @pytest.mark.asyncio @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost") async def test_httpx_mock_should_mock_async(httpx_mock): httpx_mock.add_response() async with httpx.AsyncClient() as client: # Mocked request await client.get("https://foo.tld") # Non mocked request with pytest.raises(httpx.ConnectError): await client.get("https://localhost:5005") # Assert that a single request was mocked assert len(httpx_mock.get_requests()) == 1 """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_httpx_mock_options_on_multi_levels_are_aggregated(testdir: Testdir) -> None: """ Test case ensures that every level provides one parameter that should be used in the end global (actually registered AFTER module): assert_all_responses_were_requested (tested by putting unused response) module: assert_all_requests_were_expected (tested by not mocking one URL) test: should_mock (tested by calling 3 URls, 2 mocked, the other one not) """ testdir.makeconftest( """ import pytest def pytest_collection_modifyitems(session, config, items): for item in items: item.add_marker(pytest.mark.httpx_mock(assert_all_responses_were_requested=False)) """ ) testdir.makepyfile( """ import httpx import pytest pytestmark = pytest.mark.httpx_mock(assert_all_requests_were_expected=False, should_mock=lambda request: request.url.host != "https://foo.tld") @pytest.mark.asyncio @pytest.mark.httpx_mock(should_mock=lambda request: request.url.host != "localhost") async def test_httpx_mock_options_on_multi_levels_are_aggregated(httpx_mock): httpx_mock.add_response(url="https://foo.tld", headers={"x-pytest-httpx": "this was mocked"}) # This response will never be used, testing that assert_all_responses_were_requested is handled httpx_mock.add_response(url="https://never_called.url") async with httpx.AsyncClient() as client: # Assert that previously set should_mock was overridden response = await client.get("https://foo.tld") assert response.headers["x-pytest-httpx"] == "this was mocked" # Assert that latest should_mock is handled with pytest.raises(httpx.ConnectError): await client.get("https://localhost:5005") # Assert that assert_all_requests_were_expected is the one at module level with pytest.raises(httpx.TimeoutException): await client.get("https://unexpected.url") # Assert that 2 requests out of 3 were mocked assert len(httpx_mock.get_requests()) == 2 """ ) result = testdir.runpytest() result.assert_outcomes(passed=1) def test_invalid_marker(testdir: Testdir) -> None: """ Unknown marker keyword arguments should raise a TypeError. """ testdir.makepyfile( """ import pytest @pytest.mark.httpx_mock(foo=123) def test_invalid_marker(httpx_mock): pass """ ) result = testdir.runpytest() result.assert_outcomes(errors=1) result.stdout.re_match_lines([r".*got an unexpected keyword argument 'foo'"]) def test_mandatory_response_not_matched(testdir: Testdir) -> None: """ is_optional MUST take precedence over assert_all_responses_were_requested. """ testdir.makepyfile( """ import httpx import pytest @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) def test_mandatory_response_not_matched(httpx_mock): # This response is optional and the fact that it was never requested should not trigger anything httpx_mock.add_response(url="https://test_url") # This response MUST be requested httpx_mock.add_response(url="https://test_url2", is_optional=False) """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, passed=1) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match any request on https://test_url2", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, ) def test_reusable_response_not_matched(testdir: Testdir) -> None: testdir.makepyfile( """ import httpx def test_reusable_response_not_matched(httpx_mock): httpx_mock.add_response(url="https://test_url2", is_reusable=True) """ ) result = testdir.runpytest() result.assert_outcomes(errors=1, passed=1) # Assert the teardown assertion failure result.stdout.fnmatch_lines( [ "*AssertionError: The following responses are mocked but not requested:", "* - Match every request on https://test_url2", "* ", "* If this is on purpose, refer to https://github.com/Colin-b/pytest_httpx/blob/master/README.md#allow-to-register-more-responses-than-what-will-be-requested", ], consecutive=True, )