pax_global_header00006660000000000000000000000064145654347740014535gustar00rootroot0000000000000052 comment=36c50628831767fa86b7afc4f278054c2c5037ba Colin-b-pytest_httpx-36c5062/000077500000000000000000000000001456543477400160475ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/.github/000077500000000000000000000000001456543477400174075ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/.github/workflows/000077500000000000000000000000001456543477400214445ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/.github/workflows/release.yml000066400000000000000000000010411456543477400236030ustar00rootroot00000000000000name: 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.12' - 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 }} Colin-b-pytest_httpx-36c5062/.github/workflows/test.yml000066400000000000000000000015331456543477400231500ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] pytest-major-version: ['7', '8'] 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] python -m pip install pytest~=${{ matrix.pytest-major-version }}.0 - 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 . Colin-b-pytest_httpx-36c5062/.gitignore000066400000000000000000000025371456543477400200460ustar00rootroot00000000000000# 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 Colin-b-pytest_httpx-36c5062/.pre-commit-config.yaml000066400000000000000000000001321456543477400223240ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 24.1.1 hooks: - id: blackColin-b-pytest_httpx-36c5062/CHANGELOG.md000066400000000000000000000412401456543477400176610ustar00rootroot00000000000000# 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.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.30.0...HEAD [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 Colin-b-pytest_httpx-36c5062/CONTRIBUTING.md000066400000000000000000000041701456543477400203020ustar00rootroot00000000000000# 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) Colin-b-pytest_httpx-36c5062/LICENSE000066400000000000000000000020571456543477400170600ustar00rootroot00000000000000MIT 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. Colin-b-pytest_httpx-36c5062/MANIFEST.in000066400000000000000000000000621456543477400176030ustar00rootroot00000000000000include CHANGELOG.md recursive-include tests *.py Colin-b-pytest_httpx-36c5062/README.md000066400000000000000000000603571456543477400173410ustar00rootroot00000000000000

Send responses to HTTPX using pytest

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

> 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. - [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) - [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. This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: ```python import pytest @pytest.fixture def assert_all_responses_were_requested() -> bool: return False ``` 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, the last one (according to the registration order) will be sent. 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 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") ``` #### 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 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 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` cannot be provided if `match_json` is also provided. ### 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 to stream chunks that you specify. ```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 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` 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 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. This behavior can be disabled thanks to the `assert_all_responses_were_requested` fixture: ```python import pytest @pytest.fixture def assert_all_responses_were_requested() -> bool: return False ``` Note that callbacks are considered as responses, and thus are [selected the same way](#how-response-is-selected). ### Dynamic responses Callback should return a `httpx.Response`. ```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") ``` Note that default behavior is to send an `httpx.TimeoutException` in case no response can be found. You can then test this kind of exception this way: ```python import httpx import pytest from pytest_httpx import HTTPXMock 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. 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. #### 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. #### Matching on HTTP method Use `method` parameter to specify the HTTP method (POST, PUT, DELETE, PATCH, HEAD) of the requests to retrieve. `method` parameter must be a string. It will be upper-cased, so it can be provided lower cased. Matching is performed on equality. #### 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. #### Matching on HTTP headers Use `match_headers` parameter to specify the HTTP headers executing the callback. Matching is performed on equality for each provided header. #### Matching on HTTP body Use `match_content` parameter to specify the full HTTP body executing the callback. Matching is performed on equality. ##### Matching on HTTP JSON body Use `match_json` parameter to specify the JSON decoded HTTP body executing the callback. Matching is performed on equality. You can however use `unittest.mock.ANY` to do partial matching. Note that `match_content` cannot be provided if `match_json` is also provided. ## 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 `non_mocked_hosts` fixture: ```python import pytest @pytest.fixture def non_mocked_hosts() -> list: return ["my_local_test_host", "my_other_test_host"] ``` Every other requested hosts will be mocked as in the following example ```python import pytest import httpx @pytest.fixture def non_mocked_hosts() -> list: return ["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, ) ``` Colin-b-pytest_httpx-36c5062/_config.yml000066400000000000000000000000321456543477400201710ustar00rootroot00000000000000theme: jekyll-theme-caymanColin-b-pytest_httpx-36c5062/pyproject.toml000066400000000000000000000034011456543477400207610ustar00rootroot00000000000000[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", "mock", "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", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Build Tools", "Typing :: Typed", ] dependencies = [ "httpx==0.27.*", "pytest>=7,<9", ] 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==4.*", # Used to run async tests "pytest-asyncio==0.23.*", ] [project.entry-points.pytest11] pytest_httpx = "pytest_httpx" [tool.setuptools.packages.find] exclude = ["tests*"] [tool.setuptools.dynamic] version = {attr = "pytest_httpx.version.__version__"} Colin-b-pytest_httpx-36c5062/pytest_httpx/000077500000000000000000000000001456543477400206265ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/pytest_httpx/__init__.py000066400000000000000000000040031456543477400227340ustar00rootroot00000000000000from collections.abc import Generator from typing import List import httpx import pytest from pytest import MonkeyPatch from pytest_httpx._httpx_mock import HTTPXMock from pytest_httpx._httpx_internals import IteratorStream from pytest_httpx.version import __version__ __all__ = ( "HTTPXMock", "IteratorStream", "__version__", ) @pytest.fixture def assert_all_responses_were_requested() -> bool: return True @pytest.fixture def non_mocked_hosts() -> List[str]: return [] @pytest.fixture def httpx_mock( monkeypatch: MonkeyPatch, assert_all_responses_were_requested: bool, non_mocked_hosts: List[str], ) -> Generator[HTTPXMock, None, None]: # Ensure redirections to www hosts are handled transparently. missing_www = [ f"www.{host}" for host in non_mocked_hosts if not host.startswith("www.") ] non_mocked_hosts += missing_www mock = HTTPXMock() # Mock synchronous requests real_handle_request = httpx.HTTPTransport.handle_request def mocked_handle_request( transport: httpx.HTTPTransport, request: httpx.Request ) -> httpx.Response: if request.url.host in non_mocked_hosts: return real_handle_request(transport, request) return mock._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 request.url.host in non_mocked_hosts: return await real_handle_async_request(transport, request) return await mock._handle_async_request(transport, request) monkeypatch.setattr( httpx.AsyncHTTPTransport, "handle_async_request", mocked_handle_async_request, ) yield mock mock.reset(assert_all_responses_were_requested) Colin-b-pytest_httpx-36c5062/pytest_httpx/_httpx_internals.py000066400000000000000000000035341456543477400245720ustar00rootroot00000000000000import base64 from typing import ( Union, Dict, Sequence, Tuple, Iterable, AsyncIterator, Iterator, Optional, ) 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]: for chunk in stream: yield chunk 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) Colin-b-pytest_httpx-36c5062/pytest_httpx/_httpx_mock.py000066400000000000000000000262461456543477400235310ustar00rootroot00000000000000import copy import inspect from typing import Union, Optional, Callable, Any, Awaitable import httpx from pytest_httpx import _httpx_internals from pytest_httpx._pretty_print import RequestDescription from pytest_httpx._request_matcher import _RequestMatcher class HTTPXMock: def __init__(self) -> None: 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]] ], ], ] ] = [] 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. """ 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. """ self._callbacks.append((_RequestMatcher(**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. """ 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: self._requests.append((real_transport, request)) callback = self._get_callback(real_transport, request) if callback: response = callback(request) if response: return _unread(response) raise httpx.TimeoutException( self._explain_that_no_response_was_found(real_transport, request), request=request, ) async def _handle_async_request( self, real_transport: httpx.AsyncHTTPTransport, request: httpx.Request, ) -> httpx.Response: 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) 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)}" matchers_description = "\n".join([str(matcher) for matcher in matchers]) if matchers_description: message += f" amongst:\n{matchers_description}" 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 matcher.nb_calls += 1 return callback 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. """ matcher = _RequestMatcher(**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. :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." return requests[0] if requests else None def reset(self, assert_all_responses_were_requested: bool) -> None: self._requests.clear() not_called = self._reset_callbacks() if assert_all_responses_were_requested: matchers_description = "\n".join([str(matcher) for matcher in not_called]) assert ( not not_called ), f"The following responses are mocked but not requested:\n{matchers_description}" def _reset_callbacks(self) -> list[_RequestMatcher]: callbacks_not_executed = [ matcher for matcher, _ in self._callbacks if not matcher.nb_calls ] self._callbacks.clear() return callbacks_not_executed 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 Colin-b-pytest_httpx-36c5062/pytest_httpx/_pretty_print.py000066400000000000000000000046761456543477400241170ustar00rootroot00000000000000from 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 = set( [ # 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.content is not None or matcher.json is not None for matcher in matchers ] ) self.expect_proxy = any([matcher.proxy_url is not None for matcher in matchers]) 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") return " and ".join(extra_description) Colin-b-pytest_httpx-36c5062/pytest_httpx/_request_matcher.py000066400000000000000000000114761456543477400245430ustar00rootroot00000000000000import json import re from typing import Optional, Union, Pattern, Any import httpx from pytest_httpx._httpx_internals import _proxy_url 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, 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, ): 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 if match_content is not None and match_json is not None: 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. Otherwise, use match_content." ) self.content = match_content self.json = match_json self.proxy_url = ( httpx.URL(proxy_url) if proxy_url and isinstance(proxy_url, str) else proxy_url ) 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) ) 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 None and self.json is None: return True if self.content is not None: return request.read() == self.content try: # httpx._content.encode_json hard codes utf-8 encoding. return json.loads(request.read().decode("utf-8")) == self.json except json.decoder.JSONDecodeError: return False 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 __str__(self) -> str: matcher_description = f"Match {self.method or 'all'} requests" 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.proxy_url: extra_description.append(f"{self.proxy_url} proxy URL") return " and ".join(extra_description) Colin-b-pytest_httpx-36c5062/pytest_httpx/py.typed000066400000000000000000000000001456543477400223130ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/pytest_httpx/version.py000066400000000000000000000005641456543477400226720ustar00rootroot00000000000000# 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.30.0" Colin-b-pytest_httpx-36c5062/tests/000077500000000000000000000000001456543477400172115ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/tests/__init__.py000066400000000000000000000000001456543477400213100ustar00rootroot00000000000000Colin-b-pytest_httpx-36c5062/tests/conftest.py000066400000000000000000000002051456543477400214050ustar00rootroot00000000000000# see https://docs.pytest.org/en/documentation-restructure/how-to/writing_plugins.html#testing-plugins pytest_plugins = ["pytester"] Colin-b-pytest_httpx-36c5062/tests/test_httpx_async.py000066400000000000000000002315421456543477400231750ustar00rootroot00000000000000import asyncio import math import re import time import httpx import pytest from pytest import Testdir from unittest.mock import ANY import pytest_httpx from pytest_httpx import HTTPXMock @pytest.mark.asyncio 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"" 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") 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 async def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") 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 all requests on https://test_url""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2") 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 all requests on https://test_url?a=1&a=2""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_method_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") 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 async def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") 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 requests""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_with_one_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content") 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"]), ) 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 part 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 part 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", ) 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 part 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 part 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", ) 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 part 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 part 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() 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 part 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 part 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") 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") 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() 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() 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() 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() 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() 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) 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) 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) 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) 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"]) 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"]) 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") 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") 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 pytest import httpx @pytest.mark.asyncio async def test_request_retrieval_with_more_than_one(httpx_mock): httpx_mock.add_response() 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." ] ) @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 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"}) 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 all requests with {'my-custom-header': 'value1'} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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"}) 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 all requests with {'my-custom-header': 'value1, value2'} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"user-agent": f"python-httpx/{httpx.__version__}"} ) 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 all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", } ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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"}, ) 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 requests on https://test_url?q=b with {'MyHeader': 'Something'} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @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 async def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") 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 all requests with http://my_test_proxy proxy URL""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") 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 all requests with http://my_test_proxy proxy URL""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() 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() 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() 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() 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_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") 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 all requests with b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @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 async def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) 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 all requests with {'a': 1, 'b': 2} json body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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"}, ) 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 all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio async def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) 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 all requests with {'a': 1, 'b': 2} json body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @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 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", ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @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 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", ) 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 all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @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 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", ) 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 requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @pytest.mark.asyncio 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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) @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_all_responses_were_requested=False) 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"}) async with httpx.AsyncClient() as client: tasks = [client.get("https://example.com/") 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" Colin-b-pytest_httpx-36c5062/tests/test_httpx_sync.py000066400000000000000000002005301456543477400230250ustar00rootroot00000000000000import re from unittest.mock import ANY import httpx import pytest from pytest import Testdir import pytest_httpx from pytest_httpx import HTTPXMock 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"" 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") 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"" def test_url_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url") 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 all requests on https://test_url""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_url_query_string_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url?a=1&a=2") 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 all requests on https://test_url?a=1&a=2""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_method_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") 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"" def test_method_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(method="get") 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 requests""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_with_one_response(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(url="https://test_url", content=b"test content") 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" 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"}, ) 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 requests on https://test_url?q=b with {'MyHeader': 'Something'} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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"]), ) 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", ) 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", ) 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() 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") 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") 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() 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() 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() 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() 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() 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) 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) 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"]) 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") 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() 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." ] ) 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"" def test_multi_value_headers_not_matching_single_value_issued( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(match_headers={"my-custom-header": "value1"}) 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 all requests with {'my-custom-header': 'value1'} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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"}) 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 all requests with {'my-custom-header': 'value1, value2'} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_headers_matching_respect_case(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( match_headers={"user-agent": f"python-httpx/{httpx.__version__}"} ) 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 all requests with {{'user-agent': 'python-httpx/{httpx.__version__}'}} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", } ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2', 'Host2': 'test_url'}} headers""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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"" def test_proxy_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") 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 all requests with http://my_test_proxy proxy URL""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_proxy_not_existing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(proxy_url="http://my_test_proxy") 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 all requests with http://my_test_proxy proxy URL""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_requests_retrieval_content_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response() 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() 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() 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() 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_content_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_content=b"This is the body") 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 all requests with b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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. 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"" def test_json_not_matching(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) 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 all requests with {'a': 1, 'b': 2} json body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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"}, ) 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 all requests with {'foo': 'bar'} headers and {'a': 1, 'b': 2} json body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) def test_match_json_invalid_json(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(match_json={"a": 1, "b": 2}) 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 all requests with {'a': 1, 'b': 2} json body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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"" 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", ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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"" 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", ) 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 all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 all requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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"" 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", ) 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 requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 requests on https://test_url with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=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", ) 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 requests on https://test_url2 with {{'User-Agent': 'python-httpx/{httpx.__version__}', 'Host': 'test_url2'}} headers and b'This is the body2' body""" ) # Clean up responses to avoid assertion failure httpx_mock.reset(assert_all_responses_were_requested=False) 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_all_responses_were_requested=False) 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" Colin-b-pytest_httpx-36c5062/tests/test_plugin.py000066400000000000000000000114471456543477400221270ustar00rootroot00000000000000from 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 all requests", ] ) def test_httpx_mock_unused_response_without_assertion(testdir: Testdir) -> None: """ Unused responses should not fail test case if assert_all_responses_were_requested fixture is set to False. """ testdir.makepyfile( """ import pytest @pytest.fixture def assert_all_responses_were_requested() -> bool: return 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 all requests", ] ) def test_httpx_mock_unused_callback_without_assertion(testdir: Testdir) -> None: """ Unused callbacks should not fail test case if assert_all_responses_were_requested fixture is set to False. """ testdir.makepyfile( """ import pytest @pytest.fixture def assert_all_responses_were_requested() -> bool: return 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_non_mocked_hosts_sync(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. """ testdir.makepyfile( """ import httpx import pytest @pytest.fixture def non_mocked_hosts() -> list: return ["localhost"] def test_httpx_mock_non_mocked_hosts_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_non_mocked_hosts_async(testdir: Testdir) -> None: """ Non mocked hosts should go through while other requests should be mocked. """ testdir.makepyfile( """ import httpx import pytest @pytest.fixture def non_mocked_hosts() -> list: return ["localhost"] @pytest.mark.asyncio async def test_httpx_mock_non_mocked_hosts_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)