pax_global_header 0000666 0000000 0000000 00000000064 14565434774 0014535 g ustar 00root root 0000000 0000000 52 comment=36c50628831767fa86b7afc4f278054c2c5037ba Colin-b-pytest_httpx-36c5062/ 0000775 0000000 0000000 00000000000 14565434774 0016047 5 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/.github/ 0000775 0000000 0000000 00000000000 14565434774 0017407 5 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/.github/workflows/ 0000775 0000000 0000000 00000000000 14565434774 0021444 5 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/.github/workflows/release.yml 0000664 0000000 0000000 00000001041 14565434774 0023603 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001533 14565434774 0023150 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000002537 14565434774 0020046 0 ustar 00root root 0000000 0000000 # 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.yaml 0000664 0000000 0000000 00000000132 14565434774 0022324 0 ustar 00root root 0000000 0000000 repos: - repo: https://github.com/psf/black rev: 24.1.1 hooks: - id: black Colin-b-pytest_httpx-36c5062/CHANGELOG.md 0000664 0000000 0000000 00000041240 14565434774 0017661 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000004170 14565434774 0020302 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002057 14565434774 0017060 0 ustar 00root root 0000000 0000000 MIT 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.in 0000664 0000000 0000000 00000000062 14565434774 0017603 0 ustar 00root root 0000000 0000000 include CHANGELOG.md recursive-include tests *.py Colin-b-pytest_httpx-36c5062/README.md 0000664 0000000 0000000 00000060357 14565434774 0017341 0 ustar 00root root 0000000 0000000
HTML content") with httpx.Client() as client: assert client.get("https://test_url").text == "
This isHTML 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.yml 0000664 0000000 0000000 00000000032 14565434774 0020171 0 ustar 00root root 0000000 0000000 theme: jekyll-theme-cayman Colin-b-pytest_httpx-36c5062/pyproject.toml 0000664 0000000 0000000 00000003401 14565434774 0020761 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 14565434774 0020626 5 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/pytest_httpx/__init__.py 0000664 0000000 0000000 00000004003 14565434774 0022734 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003534 14565434774 0024572 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000026246 14565434774 0023531 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000004676 14565434774 0024117 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000011476 14565434774 0024543 0 ustar 00root root 0000000 0000000 import 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.typed 0000664 0000000 0000000 00000000000 14565434774 0022313 0 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/pytest_httpx/version.py 0000664 0000000 0000000 00000000564 14565434774 0022672 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14565434774 0017211 5 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/tests/__init__.py 0000664 0000000 0000000 00000000000 14565434774 0021310 0 ustar 00root root 0000000 0000000 Colin-b-pytest_httpx-36c5062/tests/conftest.py 0000664 0000000 0000000 00000000205 14565434774 0021405 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000231542 14565434774 0023175 0 ustar 00root root 0000000 0000000 import 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"