Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pytest-httpserver for openSUSE:Factory checked in at 2023-05-24 20:21:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pytest-httpserver (Old) and /work/SRC/openSUSE:Factory/.python-pytest-httpserver.new.1533 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pytest-httpserver" Wed May 24 20:21:39 2023 rev:11 rq:1088466 version:1.0.7 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pytest-httpserver/python-pytest-httpserver.changes 2023-05-09 13:06:32.352735776 +0200 +++ /work/SRC/openSUSE:Factory/.python-pytest-httpserver.new.1533/python-pytest-httpserver.changes 2023-05-24 20:21:47.471975183 +0200 @@ -1,0 +2,25 @@ +Mon May 22 21:17:53 UTC 2023 - Dirk Müller <dmuel...@suse.com> + +- update to 1.0.7: + * With werkzeug 2.3.x the headers type has been updated to not + allow integers as header values. This restriction followed up + in pytest-httpserver. + * Python versions earlier than 3.8 have been deprecated in + order to support the latest werkzeug. + * Type hinting for header_value_matcher has been fixed. From + now, specifying a callable as ``Callable[[str, + Optional[str], str], bool]`` will be accepted also. + Providing a ``HeaderValueMatcher`` object will be also + accepted as before, as it provides the same callable signature + * Fix Werkzeug deprecation warning about + ``parse_authorization_header`` call. + * Replace ``parse_authorization_header`` with + ``Authorization.from_header`` as suggested. This fix should + not introduce any functional change for the users. + * Fix Werkzeug deprecation warning about + ``werkzeug.urls.url_decode`` call. This call has been changed + to ``urllib.parse.parse_qsl`` in the implementation. + This fix should not introduce any functional change for the + users. + +------------------------------------------------------------------- Old: ---- pytest_httpserver-1.0.6.tar.gz New: ---- pytest_httpserver-1.0.7.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pytest-httpserver.spec ++++++ --- /var/tmp/diff_new_pack.NxByS2/_old 2023-05-24 20:21:48.435980931 +0200 +++ /var/tmp/diff_new_pack.NxByS2/_new 2023-05-24 20:21:48.443980979 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-pytest-httpserver -Version: 1.0.6 +Version: 1.0.7 Release: 0 Summary: A HTTP server for pytest License: MIT ++++++ pytest_httpserver-1.0.6.tar.gz -> pytest_httpserver-1.0.7.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/CHANGES.rst new/pytest_httpserver-1.0.7/CHANGES.rst --- old/pytest_httpserver-1.0.6/CHANGES.rst 2022-09-12 09:25:14.312314500 +0200 +++ new/pytest_httpserver-1.0.7/CHANGES.rst 2023-05-16 21:40:52.284019700 +0200 @@ -2,6 +2,50 @@ Release Notes ============= +.. _Release Notes_1.0.7: + +1.0.7 +===== + +.. _Release Notes_1.0.7_Upgrade Notes: + +Upgrade Notes +------------- + +- With werkzeug 2.3.x the headers type has been updated to not allow integers as header values. This restriction followed up in pytest-httpserver. + + +.. _Release Notes_1.0.7_Deprecation Notes: + +Deprecation Notes +----------------- + +- Python versions earlier than 3.8 have been deprecated in order to support + the latest werkzeug. Users using 3.7 or earlier python may use + pytest-httpserver with earlier werkzeug versions but tests are no longer run + for these python versions. + + +.. _Release Notes_1.0.7_Bug Fixes: + +Bug Fixes +--------- + +- Type hinting for header_value_matcher has been fixed. From now, specifying a + callable as ``Callable[[str, Optional[str], str], bool]`` will be accepted + also. Providing a ``HeaderValueMatcher`` object will be also accepted as + before, as it provides the same callable signature. + +- Fix Werkzeug deprecation warning about ``parse_authorization_header`` call. + Replace ``parse_authorization_header`` with ``Authorization.from_header`` as + suggested. This fix should not introduce any functional change for the + users. + +- Fix Werkzeug deprecation warning about ``werkzeug.urls.url_decode`` call. This + call has been changed to ``urllib.parse.parse_qsl`` in the implementation. + This fix should not introduce any functional change for the users. + + .. _Release Notes_1.0.6: 1.0.6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/CONTRIBUTION.md new/pytest_httpserver-1.0.7/CONTRIBUTION.md --- old/pytest_httpserver-1.0.6/CONTRIBUTION.md 2022-09-03 10:53:55.022256900 +0200 +++ new/pytest_httpserver-1.0.7/CONTRIBUTION.md 2023-05-16 20:58:37.762110000 +0200 @@ -20,8 +20,9 @@ There are a few rules you are kindly asked to accept: -* Coding style is checked by `pre-commit`. You can run `make precommit` before proceeding - with the PR. +* Coding style is checked by `pre-commit`. You can run `make precommit` before + proceeding with the PR. To install the pre-commit hooks to your git (so it + will be run for each commit), run `pre-commit install`. * Tests should be written for the new code. If there's a complex logic implemented, it should be tested on different valid and invalid inputs and @@ -41,11 +42,15 @@ * You can let your IDE of your choice to use the `.venv/bin/python` interpreter, so it will know all the dependencies. -* running tests on the localhost can be done by issuing `make quick-test`. It is - "quick" because it tests the software with only one interpreter. Note that the +* running tests on the localhost can be done by issuing `make test`. Note that the library can be used by many supported interpreters and unless it is absolutely required, we don't want to drop support. +* running tests on multiple versions of interpreter locally can be done by + `tox`. Keep in mind that the CI job uses github actions with caching for + effective use, and `tox` is provided for the developers only. + + ## More technical details * Release notes must be written for significant changes. This is done by @@ -56,3 +61,12 @@ docstrings, but if the PR changes the code and the way of working conceptually, the main documentation (located in the doc directory) needs to be updated and extended. + +* nix files are provided on a best-effort basis. `tox.nix` can be used to run + `tox`, `shell.nix` can be used instead of poetry for development. No tests + have been written for these (yet!), so they may be out of sync occasionally. + +* to release a new version, you can use the `scripts/release.py` script to make + the wheels and sdist, generate the changelog, and tag the commit. This tool + won't upload the artifacts as they need to be checked manually (by installing + the wheel to a new venv, for example). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/PKG-INFO new/pytest_httpserver-1.0.7/PKG-INFO --- old/pytest_httpserver-1.0.6/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,22 +1,22 @@ Metadata-Version: 2.1 Name: pytest-httpserver -Version: 1.0.6 +Version: 1.0.7 Summary: pytest-httpserver is a httpserver for pytest Home-page: https://github.com/csernazs/pytest-httpserver License: MIT Author: Zsolt Cserna Author-email: cserna.zs...@gmail.com -Requires-Python: >=3.7,<4.0 +Requires-Python: >=3.8,<4.0 Classifier: Development Status :: 3 - Alpha Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: Werkzeug (>=2.0.0) Project-URL: Bug Tracker, https://github.com/csernazs/pytest-httpserver/issues diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/doc/background.rst new/pytest_httpserver-1.0.7/doc/background.rst --- old/pytest_httpserver-1.0.6/doc/background.rst 2022-07-13 18:49:54.026996000 +0200 +++ new/pytest_httpserver-1.0.7/doc/background.rst 2023-05-16 21:40:52.284019700 +0200 @@ -40,24 +40,15 @@ Example: -.. code-block:: python - - def test_query_params(httpserver): - httpserver.expect_request("/foo", query_string={"user": "user1"}).respond_with_data( - "OK" - ) +.. literalinclude :: ../tests/examples/test_example_query_params1.py + :language: python It is simple in the most simple cases, but once the expectation is more specific, the line can grow significantly, so here the user is expected to put the literals into variables: -.. code-block:: python - - def test_query_params(httpserver): - httpserver.expect_request("/foo", query_string=expected_query).respond_with_data( - "OK" - ) - +.. literalinclude :: ../tests/examples/test_example_query_params2.py + :language: python If the user wants something more complex, classes are available for this which can be instantiated and then specified for the parameters normally accepting @@ -176,12 +167,11 @@ The library should be tested periodically on the supported versions. Dropping support for old python versions is possible if supporting would cause -an issue or require extensive workaround. Currently, 3.4 is still supported by -the library, however it is deprecated by PSF. As it causes no problems for -*pytest-httpserver* (there's an additional requirement for this in the setup.py, -but that's all), the support for this version will be maintained as long as -possible. Once a new change is added to the library which require great effort -to maintain compatibility with 3.4, the support for it will be dropped. +an issue or require extensive workaround. + +Python support for a given version is also dropped if it is near to the end of +support or when a dependency deprecates it - this is needed to move forward with +the community in order to support the latest versions of the dependencies. Testing and coverage diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/doc/conf.py new/pytest_httpserver-1.0.7/doc/conf.py --- old/pytest_httpserver-1.0.6/doc/conf.py 2022-09-12 09:25:14.313314400 +0200 +++ new/pytest_httpserver-1.0.7/doc/conf.py 2023-05-16 21:40:52.284019700 +0200 @@ -66,7 +66,7 @@ # built documents. # # The short X.Y version. -version = "1.0.6" +version = "1.0.7" # The full version, including alpha/beta/rc tags. release = version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/doc/howto.rst new/pytest_httpserver-1.0.7/doc/howto.rst --- old/pytest_httpserver-1.0.6/doc/howto.rst 2022-09-12 08:46:31.629976300 +0200 +++ new/pytest_httpserver-1.0.7/doc/howto.rst 2023-05-16 20:58:37.763110000 +0200 @@ -15,17 +15,13 @@ To match query parameters, you must not included them to the URI, as this will not work: -.. code-block:: python - - def test_query_params(httpserver): - httpserver.expect_request("/foo?user=bar") # never do this +.. literalinclude :: ../tests/examples/test_howto_query_params_never_do_this.py + :language: python There's an explicit place where the query string should go: -.. code-block:: python - - def test_query_params(httpserver): - httpserver.expect_request("/foo", query_string="user=bar") +.. literalinclude :: ../tests/examples/test_howto_query_params_proper_use.py + :language: python The ``query_string`` is the parameter which does not contain the leading question mark ``?``. @@ -43,15 +39,9 @@ strange but we wanted to keep API compatibility and this dict matching feature was added later). -.. code-block:: python - - def test_query_params(httpserver): - httpserver.expect_request( - "/foo", query_string={"user": "user1", "group": "group1"} - ).respond_with_data("OK") - assert requests.get("/foo?user=user1&group=group1").status_code == 200 - assert requests.get("/foo?group=group1&user=user1").status_code == 200 +.. literalinclude :: ../tests/examples/test_howto_query_params_dict.py + :language: python In the example above, both requests pass the test as we specified the expected query string as a dictionary. @@ -70,9 +60,8 @@ If this is not desired, you can specify a regexp object (returned by the ``re.compile()`` call). -.. code:: python - - httpserver.expect_request(re.compile("^/foo"), method="GET") +.. literalinclude :: ../tests/examples/test_howto_regexp.py + :language: python The above will match every URI starting with "/foo". @@ -82,18 +71,9 @@ string and should return a boolean value. -.. code:: python - - class PrefixMatch(URIPattern): - def __init__(self, prefix: str): - self.prefix = prefix - - def match(self, uri): - return uri.startswith(self.prefix) - +.. literalinclude :: ../tests/examples/test_howto_url_matcher.py + :language: python - def test_uripattern_object(httpserver: HTTPServer): - httpserver.expect_request(PrefixMatch("/foo")).respond_with_json({"foo": "bar"}) Authentication -------------- @@ -121,50 +101,8 @@ By default, pytest-httpserver includes an Authorization header parser so the order of the parameters in the ``Authorization`` header does not matter. -.. code:: python - - def test_authorization_headers(httpserver: HTTPServer): - headers_with_values_in_direct_order = { - "Authorization": ( - 'Digest username="Mufasa",' - 'realm="testre...@host.com",' - 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' - 'uri="/dir/index.html",' - "qop=auth," - "nc=00000001," - 'cnonce="0a4f113b",' - 'response="6629fae49393a05397450978507c4ef1",' - 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' - ) - } - httpserver.expect_request( - uri="/", headers=headers_with_values_in_direct_order - ).respond_with_data("OK") - response = requests.get( - httpserver.url_for("/"), headers=headers_with_values_in_direct_order - ) - assert response.status_code == 200 - assert response.text == "OK" - - headers_with_values_in_modified_order = { - "Authorization": ( - "Digest qop=auth," - 'username="Mufasa",' - 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' - 'uri="/dir/index.html",' - "nc=00000001," - 'realm="testre...@host.com",' - 'response="6629fae49393a05397450978507c4ef1",' - 'cnonce="0a4f113b",' - 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' - ) - } - response = requests.get( - httpserver.url_for("/"), headers=headers_with_values_in_modified_order - ) - assert response.status_code == 200 - assert response.text == "OK" - +.. literalinclude :: ../tests/examples/test_howto_authorization_headers.py + :language: python JSON matching ------------- @@ -185,16 +123,8 @@ Example: -.. code:: python - - def test_json_matcher(httpserver: HTTPServer): - httpserver.expect_request("/foo", json={"foo": "bar"}).respond_with_data( - "Hello world!" - ) - resp = requests.get(httpserver.url_for("/foo"), json={"foo": "bar"}) - assert resp.status_code == 200 - assert resp.text == "Hello world!" - +.. literalinclude :: ../tests/examples/test_howto_json_matcher.py + :language: python .. note:: JSON requests usually come with ``Content-Type: application/json`` header. @@ -222,29 +152,8 @@ You need to implement such a function and then use it: -.. code:: python - - def case_insensitive_matcher(header_name: str, actual: str, expected: str) -> bool: - if header_name == "X-Foo": - return actual.lower() == expected.lower() - else: - return actual == expected - - - def test_case_insensitive_matching(httpserver: HTTPServer): - httpserver.expect_request( - "/", header_value_matcher=case_insensitive_matcher, headers={"X-Foo": "bar"} - ).respond_with_data("OK") - - assert ( - requests.get(httpserver.url_for("/"), headers={"X-Foo": "bar"}).status_code - == 200 - ) - assert ( - requests.get(httpserver.url_for("/"), headers={"X-Foo": "BAR"}).status_code - == 200 - ) - +.. literalinclude :: ../tests/examples/test_howto_case_insensitive_matcher.py + :language: python .. note:: Header value matcher is the basis of the ``Authorization`` header parsing. @@ -285,30 +194,8 @@ In case you don't want to change the defaults, you can provide the ``HeaderValueMatcher`` object itself. -.. code:: python - - from pytest_httpserver import HeaderValueMatcher - - - def case_insensitive_compare(actual: str, expected: str) -> bool: - return actual.lower() == expected.lower() - - - def test_own_matcher_object(httpserver: HTTPServer): - matcher = HeaderValueMatcher({"X-Bar": case_insensitive_compare}) - - httpserver.expect_request( - "/", headers={"X-Bar": "bar"}, header_value_matcher=matcher - ).respond_with_data("OK") - - assert ( - requests.get(httpserver.url_for("/"), headers={"X-Bar": "bar"}).status_code - == 200 - ) - assert ( - requests.get(httpserver.url_for("/"), headers={"X-Bar": "BAR"}).status_code - == 200 - ) +.. literalinclude :: ../tests/examples/test_howto_header_value_matcher.py + :language: python Using custom request handler ---------------------------- @@ -316,18 +203,9 @@ you can pass a function to the ``respond_with_handler`` function. This function will be called with a request object and it should return a Response object. -.. code:: python - - from werkzeug.wrappers import Request, Response - from random import randint - - - def test_expected_request_handler(httpserver: HTTPServer): - def handler(request: Request): - return Response(str(randint(1, 10))) - - httpserver.expect_request("/foobar").respond_with_handler(handler) +.. literalinclude :: ../tests/examples/test_howto_custom_handler.py + :language: python The above code implements a handler which returns a random number between 1 and 10. Not particularly useful but shows that the handler can return any computed @@ -344,57 +222,15 @@ Two notable examples for this: -.. code:: python - - def test_check_assertions_raises_handler_assertions(httpserver: HTTPServer): - def handler(_): - assert 1 == 2 - - httpserver.expect_request("/foobar").respond_with_handler(handler) - - requests.get(httpserver.url_for("/foobar")) - - # if you leave this "with" statement out, check_assertions() will break - # the test by re-raising the assertion error caused by the handler - # pytest will pick this exception as it was happened in the main thread - with pytest.raises(AssertionError): - httpserver.check_assertions() - - httpserver.check_handler_errors() - - - def test_check_handler_errors_raises_handler_error(httpserver: HTTPServer): - def handler(_): - raise ValueError("should be propagated") - - httpserver.expect_request("/foobar").respond_with_handler(handler) - - requests.get(httpserver.url_for("/foobar")) - - httpserver.check_assertions() - - # if you leave this "with" statement out, check_handler_errors() will - # break the test with the original exception - with pytest.raises(ValueError): - httpserver.check_handler_errors() - +.. literalinclude :: ../tests/examples/test_howto_check_handler_errors.py + :language: python If you want to call both methods (``check_handler_errors()`` and ``check_assertions()``) you can call the ``check()`` method, which will call these. - -.. code:: python - - def test_check_assertions(httpserver: HTTPServer): - def handler(_): - assert 1 == 2 - - httpserver.expect_request("/foobar").respond_with_handler(handler) - - requests.get(httpserver.url_for("/foobar")) - - httpserver.check() +.. literalinclude :: ../tests/examples/test_howto_check.py + :language: python .. note:: The scope of the errors checked by the ``check()`` method may @@ -478,24 +314,8 @@ Last, you need to assert on the ``result`` attribute of the context object. -.. code-block:: python - - def test_wait_success(httpserver: HTTPServer): - waiting_timeout = 0.1 - - with httpserver.wait(stop_on_nohandler=False, timeout=waiting_timeout) as waiting: - requests.get(httpserver.url_for("/foobar")) - httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") - requests.get(httpserver.url_for("/foobar")) - assert waiting.result - - httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") - httpserver.expect_oneshot_request("/foobaz").respond_with_data("OK foobaz") - with httpserver.wait(timeout=waiting_timeout) as waiting: - requests.get(httpserver.url_for("/foobar")) - requests.get(httpserver.url_for("/foobaz")) - assert waiting.result - +.. literalinclude :: ../tests/examples/test_howto_wait_success.py + :language: python In the above code, all the request.get() calls could be in a different thread, eg. running in parallel, but the exit condition of the context object is to wait @@ -509,19 +329,10 @@ by peer* or *Connection refused*, you can simply do it by connecting to a random port number where no service is listening: -.. code-block:: python - - import pytest - import requests - - - def test_connection_refused(): - # assumes that there's no server listening at localhost:1234 - with pytest.raises(requests.exceptions.ConnectionError): - requests.get("http://localhost:1234") - +.. literalinclude :: ../tests/examples/test_howto_timeout_requests.py + :language: python -However connecting to the port where the httpserver had been started will still +However, connecting to the port where the httpserver had been started will still succeed as the server is running continuously. This is working by design as starting/stopping the server is costly. @@ -530,6 +341,7 @@ import pytest import requests + # setting a fixed port for httpserver @pytest.fixture(scope="session") def httpserver_listen_address(): @@ -607,20 +419,46 @@ @pytest.fixture(scope="session") - def localhost_cert(ca): - return ca.issue_cert("localhost") + def httpserver_ssl_context(ca): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + localhost_cert = ca.issue_cert("localhost") + localhost_cert.configure_cert(context) + return context @pytest.fixture(scope="session") - def httpserver_ssl_context(localhost_cert): - context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + def httpclient_ssl_context(ca): + with ca.cert_pem.tempfile() as ca_temp_path: + return ssl.create_default_context(cafile=ca_temp_path) - crt = localhost_cert.cert_chain_pems[0] - key = localhost_cert.private_key_pem - with crt.tempfile() as crt_file, key.tempfile() as key_file: - context.load_cert_chain(crt_file, key_file) - return context + @pytest.mark.asyncio + async def test_aiohttp(httpserver, httpclient_ssl_context): + import aiohttp + + httpserver.expect_request("/").respond_with_data("hello world!") + connector = aiohttp.TCPConnector(ssl=httpclient_ssl_context) + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get(httpserver.url_for("/")) as result: + assert (await result.text()) == "hello world!" + + + def test_requests(httpserver, ca): + import requests + + httpserver.expect_request("/").respond_with_data("hello world!") + with ca.cert_pem.tempfile() as ca_temp_path: + result = requests.get(httpserver.url_for("/"), verify=ca_temp_path) + assert result.text == "hello world!" + + + def test_httpx(httpserver, httpclient_ssl_context): + import httpx + + httpserver.expect_request("/").respond_with_data("hello world!") + result = httpx.get(httpserver.url_for("/"), verify=httpclient_ssl_context) + assert result.text == "hello world!" + Using httpserver on a dual-stack (IPv4 and IPv6) system ------------------------------------------------------- @@ -672,5 +510,5 @@ Example: -.. literalinclude :: ../tests/test_blocking_httpserver_howto.py +.. literalinclude :: ../tests/examples/test_example_blocking_httpserver.py :language: python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/doc/tutorial.rst new/pytest_httpserver-1.0.7/doc/tutorial.rst --- old/pytest_httpserver-1.0.6/doc/tutorial.rst 2022-07-13 18:49:54.027996000 +0200 +++ new/pytest_httpserver-1.0.7/doc/tutorial.rst 2023-05-16 20:58:37.763110000 +0200 @@ -69,7 +69,7 @@ It is advised to use the ``url_for()`` method to construct an URL as it will always contain the correct port number in the URL. -If you need the http port as an integer, you can get is by the ``port`` +If you need the http port as an integer, you can get it by the ``port`` attribute of the ``httpserver`` object. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/pyproject.toml new/pytest_httpserver-1.0.7/pyproject.toml --- old/pytest_httpserver-1.0.6/pyproject.toml 2022-09-12 09:25:14.313314400 +0200 +++ new/pytest_httpserver-1.0.7/pyproject.toml 2023-05-16 21:40:52.284019700 +0200 @@ -1,6 +1,6 @@ [tool.poetry] name = "pytest_httpserver" -version = "1.0.6" +version = "1.0.7" description = "pytest-httpserver is a httpserver for pytest" authors = ["Zsolt Cserna <cserna.zs...@gmail.com>"] license = "MIT" @@ -24,7 +24,7 @@ ] [tool.poetry.dependencies] -python = ">=3.7,<4.0" +python = ">=3.8,<4.0" Werkzeug = ">= 2.0.0" @@ -46,10 +46,11 @@ mypy = "^0.971" types-requests = "^2.28.9" pytest = "^7.1.3" -pytest-cov = "^3.0.0" -coverage = "^6.4.4" -ipdb = "^0.13.9" +pytest-cov = ">=3,<5" +coverage = ">=6.4.4,<8.0.0" types-toml = "^0.10.8" +toml = "^0.10.2" +black = "^23.1.0" [tool.poetry.group.doc] @@ -65,13 +66,14 @@ [tool.poetry.group.test.dependencies] pytest = "^7.1.3" -pytest-cov = "^3.0.0" -coverage = "^6.4.4" +pytest-cov = ">=3,<5" +coverage = ">=6.4.4,<8.0.0" requests = "^2.28.1" mypy = "^0.971" types-requests = "^2.28.9" pre-commit = "^2.20.0" types-toml = "^0.10.8" +toml = "^0.10.2" [build-system] requires = ["poetry-core>=1.0.0"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/pytest_httpserver/httpserver.py new/pytest_httpserver-1.0.7/pytest_httpserver/httpserver.py --- old/pytest_httpserver-1.0.6/pytest_httpserver/httpserver.py 2022-09-07 21:46:29.731832500 +0200 +++ new/pytest_httpserver-1.0.7/pytest_httpserver/httpserver.py 2023-05-16 20:58:37.764109800 +0200 @@ -1,9 +1,11 @@ import abc +import ipaddress import json import queue import re import threading import time +import urllib.parse from collections import defaultdict from contextlib import contextmanager from contextlib import suppress @@ -21,9 +23,9 @@ from typing import Tuple from typing import Union -import werkzeug.urls +import werkzeug.http +from werkzeug.datastructures import Authorization from werkzeug.datastructures import MultiDict -from werkzeug.http import parse_authorization_header from werkzeug.serving import make_server from werkzeug.wrappers import Request from werkzeug.wrappers import Response @@ -32,10 +34,12 @@ METHOD_ALL = "__ALL" HEADERS_T = Union[ - Mapping[str, Union[str, int, Iterable[Union[str, int]]]], - Iterable[Tuple[str, Union[str, int]]], + Mapping[str, Union[str, Iterable[str]]], + Iterable[Tuple[str, str]], ] +HVMATCHER_T = Callable[[str, Optional[str], str], bool] + class Undefined: def __repr__(self): @@ -130,7 +134,10 @@ @staticmethod def authorization_header_value_matcher(actual: Optional[str], expected: str) -> bool: - return parse_authorization_header(actual) == parse_authorization_header(expected) + callable = getattr(Authorization, "from_header", None) + if callable is None: # Werkzeug < 2.3.0 + callable = werkzeug.http.parse_authorization_header + return callable(actual) == callable(expected) @staticmethod def default_header_value_matcher(actual: Optional[str], expected: str) -> bool: @@ -213,7 +220,7 @@ self.query_dict = query_dict def get_comparing_values(self, request_query_string: bytes) -> tuple: - query = werkzeug.urls.url_decode(request_query_string) + query = MultiDict(urllib.parse.parse_qsl(request_query_string.decode("utf-8"))) if isinstance(self.query_dict, MultiDict): return (query, self.query_dict) else: @@ -288,6 +295,13 @@ specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. + :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches + values of headers, or a ``Callable[[str, Optional[str], str], bool]`` + receiving the header key (from `headers`), header value (or `None`) and the expected + value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. + :param json: a python object (eg. a dict) whose value will be compared to the request body after it + is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. + If that's desired, add it to the headers parameter. """ def __init__( @@ -298,10 +312,9 @@ data_encoding: str = "utf-8", headers: Optional[Mapping[str, str]] = None, query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HeaderValueMatcher] = None, + header_value_matcher: Optional[HVMATCHER_T] = None, json: Any = UNDEFINED, ): - if json is not UNDEFINED and data is not None: raise ValueError("data and json parameters are mutually exclusive") @@ -321,7 +334,10 @@ self.data = data self.data_encoding = data_encoding - self.header_value_matcher = HeaderValueMatcher() if header_value_matcher is None else header_value_matcher + self.header_value_matcher: HVMATCHER_T = HeaderValueMatcher() + + if header_value_matcher is not None: + self.header_value_matcher = header_value_matcher def __repr__(self): """ @@ -610,6 +626,9 @@ self.ssl_context = ssl_context self.no_handler_status_code = 500 + def __repr__(self): + return f"<{self.__class__.__name__} host={self.host} port={self.port}>" + def clear(self): """ Clears and resets the state attributes of the object. @@ -650,6 +669,11 @@ This basically means that it prepends the string ``http://$HOST:$PORT/`` to the `suffix` parameter (where $HOST and $PORT are the parameters given to the constructor). + When host is an IPv6 address, the required square brackets will be added + to it, forming a valid URL. + + When SSL or TLS is in use, the protocol of the returned URL will be ``https``. + :param suffix: the suffix which will be added to the base url. It can start with ``/`` (slash) or not, the url will be the same. :return: the full url which refers to the server @@ -663,7 +687,9 @@ else: protocol = "https" - return "{}://{}:{}{}".format(protocol, self.host, self.port, suffix) + host = self.format_host(self.host) + + return "{}://{}:{}{}".format(protocol, host, self.port, suffix) def create_matcher(self, *args, **kwargs) -> RequestMatcher: """ @@ -837,6 +863,24 @@ if self.is_running(): self.stop() + @staticmethod + def format_host(host): + """ + Formats a hostname so it can be used in a URL. + Notably, this adds brackets around IPV6 addresses when + they are missing. + """ + try: + ipaddress.IPv6Address(host) + is_ipv6 = True + except ValueError: + is_ipv6 = False + + if is_ipv6 and not host.startswith("[") and not host.endswith("]"): + return f"[{host}]" + + return host + class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attributes """ @@ -910,7 +954,7 @@ data_encoding: str = "utf-8", headers: Optional[Mapping[str, str]] = None, query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HeaderValueMatcher] = None, + header_value_matcher: Optional[HVMATCHER_T] = None, handler_type: HandlerType = HandlerType.PERMANENT, json: Any = UNDEFINED, ) -> RequestHandler: @@ -944,7 +988,10 @@ specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. - :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers. + :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches + values of headers, or a ``Callable[[str, Optional[str], str], bool]`` + receiving the header key (from `headers`), header value (or `None`) and the expected + value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param handler_type: type of handler :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. @@ -982,7 +1029,7 @@ data_encoding: str = "utf-8", headers: Optional[Mapping[str, str]] = None, query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HeaderValueMatcher] = None, + header_value_matcher: Optional[HVMATCHER_T] = None, json: Any = UNDEFINED, ) -> RequestHandler: """ @@ -1004,7 +1051,10 @@ specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. - :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers. + :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches + values of headers, or a ``Callable[[str, Optional[str], str], bool]`` + receiving the header key (from `headers`), header value (or `None`) and the expected + value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. @@ -1034,7 +1084,7 @@ data_encoding: str = "utf-8", headers: Optional[Mapping[str, str]] = None, query_string: Union[None, QueryMatcher, str, bytes, Mapping] = None, - header_value_matcher: Optional[HeaderValueMatcher] = None, + header_value_matcher: Optional[HVMATCHER_T] = None, json: Any = UNDEFINED, ) -> RequestHandler: """ @@ -1056,7 +1106,10 @@ specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. - :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers. + :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches + values of headers, or a ``Callable[[str, Optional[str], str], bool]`` + receiving the header key (from `headers`), header value (or `None`) and the expected + value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/pytest_httpserver/pytest_plugin.py new/pytest_httpserver-1.0.7/pytest_httpserver/pytest_plugin.py --- old/pytest_httpserver-1.0.6/pytest_httpserver/pytest_plugin.py 2022-07-13 18:49:54.028996000 +0200 +++ new/pytest_httpserver-1.0.7/pytest_httpserver/pytest_plugin.py 2023-05-16 20:58:37.764109800 +0200 @@ -66,3 +66,37 @@ server = make_httpserver yield server server.clear() + + +@pytest.fixture(scope="session") +def make_httpserver_ipv4(httpserver_ssl_context): + server = HTTPServer(host="127.0.0.1", port=0, ssl_context=httpserver_ssl_context) + server.start() + yield server + server.clear() + if server.is_running(): + server.stop() + + +@pytest.fixture +def httpserver_ipv4(make_httpserver_ipv4): + server = make_httpserver_ipv4 + yield server + server.clear() + + +@pytest.fixture(scope="session") +def make_httpserver_ipv6(httpserver_ssl_context): + server = HTTPServer(host="::1", port=0, ssl_context=httpserver_ssl_context) + server.start() + yield server + server.clear() + if server.is_running(): + server.stop() + + +@pytest.fixture +def httpserver_ipv6(make_httpserver_ipv6): + server = make_httpserver_ipv6 + yield server + server.clear() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/setup.py new/pytest_httpserver-1.0.7/setup.py --- old/pytest_httpserver-1.0.6/setup.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/setup.py 1970-01-01 01:00:00.000000000 +0100 @@ -15,7 +15,7 @@ setup_kwargs = { 'name': 'pytest-httpserver', - 'version': '1.0.6', + 'version': '1.0.7', 'description': 'pytest-httpserver is a httpserver for pytest', 'long_description': '[](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster)\n[](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest)\n [](https://opensource.org/licenses/MIT)\n[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=K6PU3AGBZW4QC&item_name=pytest-httpserver¤cy_code=EUR&source=url)\n[](https://codecov.io/gh/csernazs/pytest-httpserver)\n[](https://github.com/psf/black)\n\n## pytest_httpserve r\n\nHTTP server for pytest\n\n\n### Nutshell\n\nThis library is designed to help to test http clients without contacting the real http server.\nIn other words, it is a fake http server which is accessible via localhost can be started with\nthe pre-defined expected http requests and their responses.\n\n### Example\n\n#### Handling a simple GET request\n```python\ndef test_my_client(\n httpserver,\n): # httpserver is a pytest fixture which starts the server\n # set up the server to serve /foobar with the json\n httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"})\n # check that the request is served\n assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"}\n```\n\n#### Handing a POST request with an expected json body\n```python\ndef test_json_request(\n httpserver,\n): # httpserver is a pytest fixture which starts the server\n # set up the server to serve /foobar with the json\n httpserver.expect_request(\n "/foo bar", method="POST", json={"id": 12, "name": "foo"}\n ).respond_with_json({"foo": "bar"})\n # check that the request is served\n assert requests.post(\n httpserver.url_for("/foobar"), json={"id": 12, "name": "foo"}\n ).json() == {"foo": "bar"}\n```\n\n\nYou can also use the library without pytest. There\'s a with statement to ensure that the server is stopped.\n\n\n```python\nwith HTTPServer() as httpserver:\n # set up the server to serve /foobar with the json\n httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"})\n # check that the request is served\n print(requests.get(httpserver.url_for("/foobar")).json())\n```\n\n### Documentation\n\nPlease find the API documentation at https://pytest-httpserver.readthedocs.io/en/latest/.\n\n### Features\n\nYou can set up a dozen of expectations for the requests, and also what response should be sent by the server to the client.\n\n\n#### Requests\n\nThere are three different types:\n\n- **permane nt**: this will be always served when there\'s match for this request, you can make as many HTTP requests as you want\n- **oneshot**: this will be served only once when there\'s a match for this request, you can only make 1 HTTP request\n- **ordered**: same as oneshot but the order must be strictly matched to the order of setting up\n\nYou can also fine-tune the expected request. The following can be specified:\n\n- URI (this is a must)\n- HTTP method\n- headers\n- query string\n- data (HTTP body of the request)\n- JSON (HTTP body loaded as JSON)\n\n\n#### Responses\n\nOnce you have the expectations for the request set up, you should also define the response you want to send back.\nThe following is supported currently:\n\n- respond arbitrary data (string or bytearray)\n- respond a json (a python dict converted in-place to json)\n- respond a Response object of werkzeug\n- use your own function\n\nSimilar to requests, you can fine-tune what response you want to send:\n\n- HTTP status\ n- headers\n- data\n\n\n#### Behave support\n\nUsing the `BlockingHTTPServer` class, the assertion for a request and the\nresponse can be performed in real order. For more info, see the\n[test](tests/test_blocking_httpserver.py), the\n[howto](https://pytest-httpserver.readthedocs.io/en/latest/howto.html#running-httpserver-in-blocking-mode)\nand the [API\ndocumentation](https://pytest-httpserver.readthedocs.io/en/latest/api.html#blockinghttpserver).\n\n\n### Missing features\n* HTTP/2\n* Keepalive\n* ~~TLS~~\n\n### Donation\n\nIf you want to donate to this project, you can find the donate button at the top\nof the README.\n\nCurrently, this project is based heavily on werkzeug. Werkzeug does all the heavy lifting\nbehind the scenes, parsing HTTP request and defining Request and Response objects, which\nare currently transparent in the API.\n\nIf you wish to donate, please consider donating to them: https://palletsprojects.com/donate\n', 'author': 'Zsolt Cserna', @@ -27,7 +27,7 @@ 'package_data': package_data, 'install_requires': install_requires, 'entry_points': entry_points, - 'python_requires': '>=3.7,<4.0', + 'python_requires': '>=3.8,<4.0', } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_example_blocking_httpserver.py new/pytest_httpserver-1.0.7/tests/examples/test_example_blocking_httpserver.py --- old/pytest_httpserver-1.0.6/tests/examples/test_example_blocking_httpserver.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_example_blocking_httpserver.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,59 @@ +import threading +from queue import Queue + +import pytest +import requests + +from pytest_httpserver import BlockingHTTPServer + +# override httpserver fixture + + +@pytest.fixture +def httpserver(): + server = BlockingHTTPServer(timeout=1) + server.start() + + yield server + + server.clear() + if server.is_running(): + server.stop() + + # this is to check if the client has made any request where no + # `assert_request` was called on it from the test + + server.check_assertions() + server.clear() + + +def test_simplified(httpserver: BlockingHTTPServer): + def client(response_queue: Queue): + response = requests.get(httpserver.url_for("/foobar"), timeout=10) + response_queue.put(response) + + # start the client, server is not yet configured + # it will block until we add a request handler to the server + # (see the timeout parameter of the http server) + response_queue: Queue[requests.models.Response] = Queue(maxsize=1) + thread = threading.Thread(target=client, args=(response_queue,)) + thread.start() + + try: + # check that the request is for /foobar and it is a GET method + # if this does not match, it will raise AssertionError and test will fail + client_connection = httpserver.assert_request(uri="/foobar", method="GET") + + # with the received client_connection, we now need to send back the response + # this makes the request.get() call in client() to return + client_connection.respond_with_json({"foo": "bar"}) + + finally: + # wait for the client thread to complete + thread.join(timeout=1) + assert not thread.is_alive() # check if join() has not timed out + + # check the response the client received + response = response_queue.get(timeout=1) + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_example_query_params1.py new/pytest_httpserver-1.0.7/tests/examples/test_example_query_params1.py --- old/pytest_httpserver-1.0.6/tests/examples/test_example_query_params1.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_example_query_params1.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,2 @@ +def test_query_params(httpserver): + httpserver.expect_request("/foo", query_string={"user": "user1"}).respond_with_data("OK") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_example_query_params2.py new/pytest_httpserver-1.0.7/tests/examples/test_example_query_params2.py --- old/pytest_httpserver-1.0.6/tests/examples/test_example_query_params2.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_example_query_params2.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,3 @@ +def test_query_params(httpserver): + expected_query = {"user": "user1"} + httpserver.expect_request("/foo", query_string=expected_query).respond_with_data("OK") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_authorization_headers.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_authorization_headers.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_authorization_headers.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_authorization_headers.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,40 @@ +import requests + +from pytest_httpserver import HTTPServer + + +def test_authorization_headers(httpserver: HTTPServer): + headers_with_values_in_direct_order = { + "Authorization": ( + 'Digest username="Mufasa",' + 'realm="testre...@host.com",' + 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' + 'uri="/dir/index.html",' + "qop=auth," + "nc=00000001," + 'cnonce="0a4f113b",' + 'response="6629fae49393a05397450978507c4ef1",' + 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' + ) + } + httpserver.expect_request(uri="/", headers=headers_with_values_in_direct_order).respond_with_data("OK") + response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_direct_order) + assert response.status_code == 200 + assert response.text == "OK" + + headers_with_values_in_modified_order = { + "Authorization": ( + "Digest qop=auth," + 'username="Mufasa",' + 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' + 'uri="/dir/index.html",' + "nc=00000001," + 'realm="testre...@host.com",' + 'response="6629fae49393a05397450978507c4ef1",' + 'cnonce="0a4f113b",' + 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' + ) + } + response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_modified_order) + assert response.status_code == 200 + assert response.text == "OK" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_case_insensitive_matcher.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_case_insensitive_matcher.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_case_insensitive_matcher.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_case_insensitive_matcher.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,24 @@ +from typing import Optional + +import requests + +from pytest_httpserver import HTTPServer + + +def case_insensitive_matcher(header_name: str, actual: Optional[str], expected: str) -> bool: + if actual is None: + return False + + if header_name == "X-Foo": + return actual.lower() == expected.lower() + else: + return actual == expected + + +def test_case_insensitive_matching(httpserver: HTTPServer): + httpserver.expect_request( + "/", header_value_matcher=case_insensitive_matcher, headers={"X-Foo": "bar"} + ).respond_with_data("OK") + + assert requests.get(httpserver.url_for("/"), headers={"X-Foo": "bar"}).status_code == 200 + assert requests.get(httpserver.url_for("/"), headers={"X-Foo": "BAR"}).status_code == 200 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_check.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_check.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_check.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_check.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,17 @@ +import pytest +import requests + +from pytest_httpserver import HTTPServer + + +@pytest.mark.xfail +def test_check_assertions(httpserver: HTTPServer): + def handler(_): + assert 1 == 2 + + httpserver.expect_request("/foobar").respond_with_handler(handler) + + requests.get(httpserver.url_for("/foobar")) + + # this will raise AssertionError: + httpserver.check() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_check_handler_errors.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_check_handler_errors.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_check_handler_errors.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_check_handler_errors.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,37 @@ +import pytest +import requests + +from pytest_httpserver import HTTPServer + + +def test_check_assertions_raises_handler_assertions(httpserver: HTTPServer): + def handler(_): + assert 1 == 2 + + httpserver.expect_request("/foobar").respond_with_handler(handler) + + requests.get(httpserver.url_for("/foobar")) + + # if you leave this "with" statement out, check_assertions() will break + # the test by re-raising the assertion error caused by the handler + # pytest will pick this exception as it was happened in the main thread + with pytest.raises(AssertionError): + httpserver.check_assertions() + + httpserver.check_handler_errors() + + +def test_check_handler_errors_raises_handler_error(httpserver: HTTPServer): + def handler(_): + raise ValueError("should be propagated") + + httpserver.expect_request("/foobar").respond_with_handler(handler) + + requests.get(httpserver.url_for("/foobar")) + + httpserver.check_assertions() + + # if you leave this "with" statement out, check_handler_errors() will + # break the test with the original exception + with pytest.raises(ValueError): + httpserver.check_handler_errors() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_custom_handler.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_custom_handler.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_custom_handler.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_custom_handler.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,13 @@ +from random import randint + +from werkzeug.wrappers import Request +from werkzeug.wrappers import Response + +from pytest_httpserver import HTTPServer + + +def test_expected_request_handler(httpserver: HTTPServer): + def handler(request: Request): + return Response(str(randint(1, 10))) + + httpserver.expect_request("/foobar").respond_with_handler(handler) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_header_value_matcher.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_header_value_matcher.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_header_value_matcher.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_header_value_matcher.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,22 @@ +from typing import Optional + +import requests + +from pytest_httpserver import HeaderValueMatcher +from pytest_httpserver import HTTPServer + + +def case_insensitive_compare(actual: Optional[str], expected: str) -> bool: + # actual is `None` if it is not specified + if actual is None: + return False + return actual.lower() == expected.lower() + + +def test_own_matcher_object(httpserver: HTTPServer): + matcher = HeaderValueMatcher({"X-Bar": case_insensitive_compare}) + + httpserver.expect_request("/", headers={"X-Bar": "bar"}, header_value_matcher=matcher).respond_with_data("OK") + + assert requests.get(httpserver.url_for("/"), headers={"X-Bar": "bar"}).status_code == 200 + assert requests.get(httpserver.url_for("/"), headers={"X-Bar": "BAR"}).status_code == 200 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_json_matcher.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_json_matcher.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_json_matcher.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_json_matcher.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,10 @@ +import requests + +from pytest_httpserver import HTTPServer + + +def test_json_matcher(httpserver: HTTPServer): + httpserver.expect_request("/foo", json={"foo": "bar"}).respond_with_data("Hello world!") + resp = requests.get(httpserver.url_for("/foo"), json={"foo": "bar"}) + assert resp.status_code == 200 + assert resp.text == "Hello world!" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_query_params_dict.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_query_params_dict.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_query_params_dict.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_query_params_dict.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,8 @@ +import requests + + +def test_query_params(httpserver): + httpserver.expect_request("/foo", query_string={"user": "user1", "group": "group1"}).respond_with_data("OK") + + assert requests.get(httpserver.url_for("/foo?user=user1&group=group1")).status_code == 200 + assert requests.get(httpserver.url_for("/foo?group=group1&user=user1")).status_code == 200 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_query_params_never_do_this.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_query_params_never_do_this.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_query_params_never_do_this.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_query_params_never_do_this.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,2 @@ +def test_query_params(httpserver): + httpserver.expect_request("/foo?user=bar") # never do this diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_query_params_proper_use.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_query_params_proper_use.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_query_params_proper_use.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_query_params_proper_use.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,2 @@ +def test_query_params(httpserver): + httpserver.expect_request("/foo", query_string="user=bar") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_regexp.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_regexp.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_regexp.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_regexp.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,8 @@ +import re + +import requests + + +def test_httpserver_with_regexp(httpserver): + httpserver.expect_request(re.compile("^/foo"), method="GET") + requests.get(httpserver.url_for("/foobar")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_timeout_requests.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_timeout_requests.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_timeout_requests.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_timeout_requests.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,8 @@ +import pytest +import requests + + +def test_connection_refused(): + # assumes that there's no server listening at localhost:1234 + with pytest.raises(requests.exceptions.ConnectionError): + requests.get("http://localhost:1234") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_url_matcher.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_url_matcher.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_url_matcher.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_url_matcher.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,14 @@ +from pytest_httpserver import HTTPServer +from pytest_httpserver import URIPattern + + +class PrefixMatch(URIPattern): + def __init__(self, prefix: str): + self.prefix = prefix + + def match(self, uri): + return uri.startswith(self.prefix) + + +def test_uripattern_object(httpserver: HTTPServer): + httpserver.expect_request(PrefixMatch("/foo")).respond_with_json({"foo": "bar"}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/examples/test_howto_wait_success.py new/pytest_httpserver-1.0.7/tests/examples/test_howto_wait_success.py --- old/pytest_httpserver-1.0.6/tests/examples/test_howto_wait_success.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/examples/test_howto_wait_success.py 2023-05-16 20:58:37.764109800 +0200 @@ -0,0 +1,20 @@ +import requests + +from pytest_httpserver import HTTPServer + + +def test_wait_success(httpserver: HTTPServer): + waiting_timeout = 0.1 + + with httpserver.wait(stop_on_nohandler=False, timeout=waiting_timeout) as waiting: + requests.get(httpserver.url_for("/foobar")) + httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") + requests.get(httpserver.url_for("/foobar")) + assert waiting.result + + httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") + httpserver.expect_oneshot_request("/foobaz").respond_with_data("OK foobaz") + with httpserver.wait(timeout=waiting_timeout) as waiting: + requests.get(httpserver.url_for("/foobar")) + requests.get(httpserver.url_for("/foobaz")) + assert waiting.result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_blocking_httpserver.py new/pytest_httpserver-1.0.7/tests/test_blocking_httpserver.py --- old/pytest_httpserver-1.0.6/tests/test_blocking_httpserver.py 2022-09-09 09:40:14.155206700 +0200 +++ new/pytest_httpserver-1.0.7/tests/test_blocking_httpserver.py 2023-05-16 20:58:37.765109800 +0200 @@ -59,7 +59,6 @@ ) with when_a_request_is_being_sent_to_the_server(request) as server_connection: - client_connection = then_the_server_gets_the_request(httpserver, request) response = {"foo": "bar"} @@ -76,7 +75,6 @@ ) with when_a_request_is_being_sent_to_the_server(request): - with pytest.raises(AssertionError) as exc: httpserver.assert_request(uri="/not/my/path/") @@ -99,7 +97,6 @@ ) with when_a_request_is_being_sent_to_the_server(request) as server_connection: - assert server_connection.get(timeout=9).text == "No handler found for this request" @@ -110,7 +107,6 @@ ) with when_a_request_is_being_sent_to_the_server(request): - then_the_server_gets_the_request(httpserver, request) httpserver.stop() # waiting for timeout of waiting for the response @@ -120,3 +116,7 @@ assert "/my/path" in str(exc) assert "no response" in str(exc).lower() + + +def test_repr(httpserver: BlockingHTTPServer): + assert repr(httpserver) == f"<BlockingHTTPServer host=localhost port={httpserver.port}>" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_blocking_httpserver_howto.py new/pytest_httpserver-1.0.7/tests/test_blocking_httpserver_howto.py --- old/pytest_httpserver-1.0.6/tests/test_blocking_httpserver_howto.py 2022-09-12 08:46:31.629976300 +0200 +++ new/pytest_httpserver-1.0.7/tests/test_blocking_httpserver_howto.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,59 +0,0 @@ -import threading -from queue import Queue - -import pytest -import requests - -from pytest_httpserver import BlockingHTTPServer - -# override httpserver fixture - - -@pytest.fixture -def httpserver(): - server = BlockingHTTPServer(timeout=1) - server.start() - - yield server - - server.clear() - if server.is_running(): - server.stop() - - # this is to check if the client has made any request where no - # `assert_request` was called on it from the test - - server.check_assertions() - server.clear() - - -def test_simplified(httpserver: BlockingHTTPServer): - def client(response_queue: Queue): - response = requests.get(httpserver.url_for("/foobar"), timeout=10) - response_queue.put(response) - - # start the client, server is not yet configured - # it will block until we add a request handler to the server - # (see the timeout parameter of the http server) - response_queue: Queue[requests.models.Response] = Queue(maxsize=1) - thread = threading.Thread(target=client, args=(response_queue,)) - thread.start() - - try: - # check that the request is for /foobar and it is a GET method - # if this does not match, it will raise AssertionError and test will fail - client_connection = httpserver.assert_request(uri="/foobar", method="GET") - - # with the received client_connection, we now need to send back the response - # this makes the request.get() call in client() to return - client_connection.respond_with_json({"foo": "bar"}) - - finally: - # wait for the client thread to complete - thread.join(timeout=1) - assert not thread.is_alive() # check if join() has not timed out - - # check the response the client received - response = response_queue.get(timeout=1) - assert response.status_code == 200 - assert response.json() == {"foo": "bar"} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_headers.py new/pytest_httpserver-1.0.7/tests/test_headers.py --- old/pytest_httpserver-1.0.6/tests/test_headers.py 2022-07-13 18:49:54.028996000 +0200 +++ new/pytest_httpserver-1.0.7/tests/test_headers.py 2023-05-16 20:58:37.765109800 +0200 @@ -82,15 +82,14 @@ def test_header_one_key_multiple_values(httpserver: HTTPServer): httpserver.expect_request(uri="/t1").respond_with_data(headers=[("X-Foo", "123"), ("X-Foo", "456")]) httpserver.expect_request(uri="/t2").respond_with_data(headers={"X-Foo": ["123", "456"]}) - httpserver.expect_request(uri="/t3").respond_with_data(headers={"X-Foo": [123, 456]}) headers = Headers() headers.add("X-Foo", "123") headers.add("X-Foo", "456") - httpserver.expect_request(uri="/t4").respond_with_data(headers=headers) + httpserver.expect_request(uri="/t3").respond_with_data(headers=headers) - for uri in ("/t1", "/t2", "/t3", "/t4"): + for uri in ("/t1", "/t2", "/t3"): conn = http.client.HTTPConnection("localhost:{}".format(httpserver.port)) conn.request("GET", uri) response = conn.getresponse() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_ip_protocols.py new/pytest_httpserver-1.0.7/tests/test_ip_protocols.py --- old/pytest_httpserver-1.0.6/tests/test_ip_protocols.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/test_ip_protocols.py 2023-05-16 20:58:37.765109800 +0200 @@ -0,0 +1,18 @@ +import requests + + +def test_ipv4(httpserver_ipv4): + httpserver_ipv4.expect_request("/").respond_with_data("OK") + assert httpserver_ipv4.host == "127.0.0.1" + + response = requests.get(httpserver_ipv4.url_for("/")) + assert response.text == "OK" + + +def test_ipv6(httpserver_ipv6): + httpserver_ipv6.expect_request("/").respond_with_data("OK") + assert httpserver_ipv6.host == "::1" + assert httpserver_ipv6.url_for("/") == f"http://[::1]:{httpserver_ipv6.port}/" + + response = requests.get(httpserver_ipv6.url_for("/")) + assert response.text == "OK" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_mixed.py new/pytest_httpserver-1.0.7/tests/test_mixed.py --- old/pytest_httpserver-1.0.6/tests/test_mixed.py 2022-07-13 18:49:54.029996000 +0200 +++ new/pytest_httpserver-1.0.7/tests/test_mixed.py 2023-05-16 20:58:37.765109800 +0200 @@ -82,3 +82,7 @@ assert len(httpserver.ordered_handlers) == 2 assert len(httpserver.oneshot_handlers) == 2 assert len(httpserver.handlers) == 1 + + +def test_repr(httpserver: HTTPServer): + assert repr(httpserver) == f"<HTTPServer host=localhost port={httpserver.port}>" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_parse_qs.py new/pytest_httpserver-1.0.7/tests/test_parse_qs.py --- old/pytest_httpserver-1.0.6/tests/test_parse_qs.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pytest_httpserver-1.0.7/tests/test_parse_qs.py 2023-05-16 20:58:37.765109800 +0200 @@ -0,0 +1,30 @@ +import urllib.parse +from typing import List +from typing import Tuple + +import pytest +import werkzeug.urls +from werkzeug.datastructures import MultiDict + +parse_qsl_semicolon_cases = [ + ("&", []), + ("&&", []), + ("&a=b", [("a", "b")]), + ("a=a+b&b=b+c", [("a", "a b"), ("b", "b c")]), + ("a=1&a=2", [("a", "1"), ("a", "2")]), + ("a=", [("a", "")]), + ("a=foo bar&b=bar foo", [("a", "foo bar"), ("b", "bar foo")]), + ("a=foo%20bar&b=bar%20foo", [("a", "foo bar"), ("b", "bar foo")]), + ("a=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D", [("a", " !\"#$%&'()*+,/:;=?@[]")]), +] + + +@pytest.mark.parametrize("qs,expected", parse_qsl_semicolon_cases) +def test_qsl(qs: str, expected: List[Tuple[bytes, bytes]]): + assert urllib.parse.parse_qsl(qs, keep_blank_values=True) == expected + + +@pytest.mark.skip(reason="skipped to avoid werkzeug warnings") +@pytest.mark.parametrize("qs,expected", parse_qsl_semicolon_cases) +def test_qsl_werkzeug(qs: str, expected: List[Tuple[bytes, bytes]]): + assert werkzeug.urls.url_decode(qs) == MultiDict(expected) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_permanent.py new/pytest_httpserver-1.0.7/tests/test_permanent.py --- old/pytest_httpserver-1.0.6/tests/test_permanent.py 2022-07-13 18:49:54.029996000 +0200 +++ new/pytest_httpserver-1.0.7/tests/test_permanent.py 2023-05-16 20:58:37.765109800 +0200 @@ -93,3 +93,16 @@ assert httpserver.ordered_handlers == [] assert httpserver.oneshot_handlers == [] assert httpserver.handlers == [] + + +def test_response_handler_replaced(httpserver: HTTPServer): + # https://github.com/csernazs/pytest-httpserver/issues/229 + handler = httpserver.expect_request("/foobar") + handler.respond_with_data("FOO") + response = requests.get(httpserver.url_for("/foobar")) + assert response.text == "FOO" + assert response.status_code == 200 + handler.respond_with_json({"foo": "bar"}) + response = requests.get(httpserver.url_for("/foobar")) + assert response.json() == {"foo": "bar"} + assert response.status_code == 200 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_httpserver-1.0.6/tests/test_release.py new/pytest_httpserver-1.0.7/tests/test_release.py --- old/pytest_httpserver-1.0.6/tests/test_release.py 2022-09-12 08:46:48.840120300 +0200 +++ new/pytest_httpserver-1.0.7/tests/test_release.py 2023-05-16 20:58:37.765109800 +0200 @@ -78,7 +78,7 @@ @pytest.fixture(scope="session") -def build(request) -> Iterable[Build]: +def build() -> Iterable[Build]: dist_path = Path("dist").resolve() if dist_path.is_dir(): shutil.rmtree(dist_path) @@ -195,15 +195,17 @@ "tests": { "assets", "conftest.py", + "examples", "test_blocking_httpserver.py", - "test_blocking_httpserver_howto.py", "test_handler_errors.py", "test_headers.py", + "test_ip_protocols.py", "test_json_matcher.py", "test_mixed.py", "test_oneshot.py", "test_ordered.py", "test_permanent.py", + "test_parse_qs.py", "test_port_changing.py", "test_querymatcher.py", "test_querystring.py",