Repository: urllib3/urllib3

Issue:
Create a mechanism to test different HTTP protocols (HTTP/1.1 and HTTP/2) with the same test case
Currently our test suite only has to worry about HTTP/1.1, but with HTTP/2 support coming we're going to need to run tests both in HTTP/1.1 and HTTP/2 modes to avoid duplicating the entire test suite for the high-level tests like for `PoolManager`.

My proposal for the mechanism would be to control Hypercorn's ALPN to only offer `http/1.1` or `h2` depending on the test suite and then either run tests twice using a fixture or allowing external control somehow (environment variable?) and then running a separate job on CI? Open to other ideas too!


Discussion:

Resolving patch:
diff --git a/dummyserver/socketserver.py b/dummyserver/socketserver.py
--- a/dummyserver/socketserver.py
+++ b/dummyserver/socketserver.py
@@ -20,7 +20,7 @@
 from cryptography.hazmat.primitives import serialization
 
 from urllib3.exceptions import HTTPWarning
-from urllib3.util import ALPN_PROTOCOLS, resolve_cert_reqs, resolve_ssl_version
+from urllib3.util import resolve_cert_reqs, resolve_ssl_version
 
 if typing.TYPE_CHECKING:
     from typing_extensions import ParamSpec
@@ -35,7 +35,7 @@
     "keyfile": os.path.join(CERTS_PATH, "server.key"),
     "cert_reqs": ssl.CERT_OPTIONAL,
     "ca_certs": os.path.join(CERTS_PATH, "cacert.pem"),
-    "alpn_protocols": ALPN_PROTOCOLS,
+    "alpn_protocols": ["h2", "http/1.1"],
 }
 DEFAULT_CA = os.path.join(CERTS_PATH, "cacert.pem")
 DEFAULT_CA_KEY = os.path.join(CERTS_PATH, "cacert.key")


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -10,6 +10,7 @@
 import pytest
 import trustme
 
+import urllib3.http2
 from dummyserver.app import hypercorn_app
 from dummyserver.asgi_proxy import ProxyApp
 from dummyserver.hypercornserver import run_hypercorn_in_thread
@@ -369,3 +370,14 @@ def requires_tlsv1_3(supported_tls_versions: typing.AbstractSet[str]) -> None:
         or "TLSv1.3" not in supported_tls_versions
     ):
         pytest.skip("Test requires TLSv1.3")
+
+
+@pytest.fixture(params=["h11", "h2"])
+def http_version(request: pytest.FixtureRequest) -> typing.Generator[str, None, None]:
+    if request.param == "h2":
+        urllib3.http2.inject_into_urllib3()
+
+    yield request.param
+
+    if request.param == "h2":
+        urllib3.http2.extract_from_urllib3()
diff --git a/test/with_dummyserver/test_http2.py b/test/with_dummyserver/test_http2.py
deleted file mode 100644
--- a/test/with_dummyserver/test_http2.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from __future__ import annotations
-
-import subprocess
-from test import notWindows
-from test.conftest import ServerConfig
-
-import pytest
-
-import urllib3
-from dummyserver.socketserver import DEFAULT_CERTS
-from dummyserver.testcase import HTTPSHypercornDummyServerTestCase
-
-DEFAULT_CERTS_HTTP2 = DEFAULT_CERTS.copy()
-DEFAULT_CERTS_HTTP2["alpn_protocols"] = ["h2"]
-
-
-def setup_module() -> None:
-    try:
-        from urllib3.http2 import inject_into_urllib3
-
-        inject_into_urllib3()
-    except ImportError as e:
-        pytest.skip(f"Could not import h2: {e!r}")
-
-
-def teardown_module() -> None:
-    try:
-        from urllib3.http2 import extract_from_urllib3
-
-        extract_from_urllib3()
-    except ImportError:
-        pass
-
-
-class TestHypercornDummyServerTestCase(HTTPSHypercornDummyServerTestCase):
-    certs = DEFAULT_CERTS_HTTP2
-
-    @classmethod
-    def setup_class(cls) -> None:
-        super().setup_class()
-        cls.base_url = f"https://{cls.host}:{cls.port}"
-
-    @notWindows()  # GitHub Actions Windows doesn't have HTTP/2 support.
-    def test_hypercorn_server_http2(self) -> None:
-        # This is a meta test to make sure our Hypercorn test server is actually using HTTP/2
-        # before urllib3 is capable of speaking HTTP/2. Thanks, Daniel! <3
-        output = subprocess.check_output(
-            [
-                "curl",
-                "-vvv",
-                "--http2",
-                "--cacert",
-                self.certs["ca_certs"],
-                self.base_url,
-            ],
-            stderr=subprocess.STDOUT,
-        )
-
-        assert b"< HTTP/2 200" in output
-        assert output.endswith(b"Dummy server!")
-
-
-def test_simple_http2(san_server: ServerConfig) -> None:
-    with urllib3.PoolManager(ca_certs=san_server.ca_certs) as http:
-        resp = http.request("HEAD", san_server.base_url, retries=False)
-
-    assert resp.status == 200
-    resp.headers.pop("date")
-    assert resp.headers == {
-        "content-type": "text/html; charset=utf-8",
-        "content-length": "13",
-        "server": "hypercorn-h2",
-    }
diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py
--- a/test/with_dummyserver/test_https.py
+++ b/test/with_dummyserver/test_https.py
@@ -129,7 +129,7 @@ def teardown_class(cls) -> None:
 
         shutil.rmtree(cls.certs_dir)
 
-    def test_simple(self) -> None:
+    def test_simple(self, http_version: str) -> None:
         with HTTPSConnectionPool(
             self.host,
             self.port,
@@ -138,6 +138,7 @@ def test_simple(self) -> None:
         ) as https_pool:
             r = https_pool.request("GET", "/")
             assert r.status == 200, r.data
+            assert r.headers["server"] == f"hypercorn-{http_version}"
 
     @resolvesLocalhostFQDN()
     def test_dotted_fqdn(self) -> None:
@@ -1130,7 +1131,7 @@ def test_can_validate_ip_san(self, ipv4_san_server: ServerConfig) -> None:
 class TestHTTPS_IPV6SAN:
     @pytest.mark.parametrize("host", ["::1", "[::1]"])
     def test_can_validate_ipv6_san(
-        self, ipv6_san_server: ServerConfig, host: str
+        self, ipv6_san_server: ServerConfig, host: str, http_version: str
     ) -> None:
         """Ensure that urllib3 can validate SANs with IPv6 addresses in them."""
         with HTTPSConnectionPool(
@@ -1141,3 +1142,4 @@ def test_can_validate_ipv6_san(
         ) as https_pool:
             r = https_pool.request("GET", "/")
             assert r.status == 200
+            assert r.headers["server"] == f"hypercorn-{http_version}"