]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
pytest: fixes and tidy-ups to h3-proxy tests
authorViktor Szakats <commit@vsz.me>
Sat, 30 May 2026 08:53:21 +0000 (10:53 +0200)
committerViktor Szakats <commit@vsz.me>
Mon, 1 Jun 2026 13:07:17 +0000 (15:07 +0200)
- merge tests into a single class.
  For shorter names, to fix sort order by test number, and to align with
  other tests.
- fix preconditions to make `test_60_04_guard_proxy_http3_unsupported`
  actually run.
- replace local precondition with constant of the same effect.
- drop redundant non-`ngtcp2` requirement for
  `test_60_04_guard_proxy_http3_unsupported`.
  (seemed relevant for no longer supported openssl-quic builds.)
- drop unused `NGTCP2_ONLY_MSG` constant.
  Follow-up to e4139a73c82d2035142f5ae36196adb4e9831dae #21798
- avoid creating unnecessary test data blobs, and minimize their scopes.

Follow-up to 91facd7bb3bb366525b7cb41221f6359c5e936db #21791
Follow-up to e78b1b3eccfa6a2e367a1225ea1b66dafcdac3c4 #21153

Closes #21811

tests/http/test_60_h3_proxy.py

index 4d1998165049d8b2ef45423bd0af0a4821f18c1f..7ebba8a2b19dde91e2d411e27b02677054589587 100644 (file)
@@ -55,14 +55,6 @@ MARK_NEEDS_NGHTTPX = pytest.mark.skipif(
     condition=not Env.have_nghttpx(), reason="no nghttpx available"
 )
 
-H3_PROXY_COMMON_MARKS = [
-    MARK_NEEDS_HTTPS_PROXY,
-    MARK_NEEDS_HTTP3,
-    MARK_NEEDS_PROXY_HTTP3,
-    MARK_NEEDS_NGHTTP3,
-]
-
-NGTCP2_ONLY_MSG = "only supported with the ngtcp2 quic stack"
 UNSUPPORTED_OPT_MSG = "does not support this"
 H2O_HELLO_MSG = '"message": "Hello from h2o HTTP/3 server"'
 
@@ -145,11 +137,15 @@ def _h2o_proxy_args(
     return xargs
 
 
-class TestH3ProxySuccess:
-    """Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP."""
+@MARK_NEEDS_HTTPS_PROXY
+@MARK_NEEDS_HTTP3
+@MARK_NEEDS_NGHTTP3
+class TestH3Proxy:
 
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
+    # Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP.
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     @pytest.mark.parametrize(
         ["alpn_proto", "proxy_proto"],
         [
@@ -192,12 +188,10 @@ class TestH3ProxySuccess:
         r.check_response(count=1, http_status=200)
         _check_download_message(curl, H2O_HELLO_MSG)
 
+    # Failure matrix when proxy side does not support requested mode.
 
-class TestH3ProxyFailure:
-    """Failure matrix when proxy side does not support requested mode."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX]
-
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_NGHTTPX
     @pytest.mark.parametrize(
         ["alpn_proto", "proxy_proto", "exp_err"],
         [
@@ -260,12 +254,10 @@ class TestH3ProxyFailure:
             f"Expected protocol/proxy error but got: {r.dump_logs()}"
         )
 
+    # Behavior checks for tunnel vs non-tunnel proxy mode selection.
 
-class TestH3ProxyModeSelection:
-    """Behavior checks for tunnel vs non-tunnel proxy mode selection."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX]
-
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_NGHTTPX
     @pytest.mark.parametrize(
         ["proxy_proto"],
         [
@@ -301,22 +293,8 @@ class TestH3ProxyModeSelection:
             f"expected CONNECT-UDP attempt in output, got: {r.dump_logs()}"
         )
 
-
-class TestH3ProxyRuntimeGuards:
-    """Guard checks for unsupported HTTP/3 proxy options."""
-
-    pytestmark = [
-        MARK_NEEDS_HTTPS_PROXY,
-        MARK_NEEDS_PROXY_HTTP3,
-        pytest.mark.skipif(
-            condition=Env.curl_uses_lib("ngtcp2"),
-            reason="guard only applies to non-ngtcp2 builds",
-        ),
-    ]
+    # Guard checks for unsupported HTTP/3 proxy options.
 
-    @pytest.mark.skipif(
-        condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support"
-    )
     @pytest.mark.skipif(
         condition=Env.curl_has_feature("proxy-HTTP3"), reason="curl has h3 proxy support"
     )
@@ -340,23 +318,11 @@ class TestH3ProxyRuntimeGuards:
             f"Expected unsupported option failure but got: {r.stderr}"
         )
 
+    # Robustness checks for shutdown and proxy loss during transfer.
 
-class TestH3ProxyRobustness:
-    """Robustness checks for shutdown and proxy loss during transfer."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
-    @pytest.fixture(autouse=True, scope="class")
-    def _class_scope(self, env, h2o_server):
-        if not env.have_h2o():
-            pytest.skip("h2o not available")
-        env.make_data_file(
-            indir=h2o_server.docs_dir, fname="proxy-drop-20m", fsize=20 * 1024 * 1024
-        )
-
-    def test_60_05_graceful_shutdown(
-        self, env: Env, h2o_server, h2o_proxy
-    ):
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
+    def test_60_05_graceful_shutdown(self, env: Env, h2o_server, h2o_proxy):
         if not env.curl_is_debug():
             pytest.skip("needs debug curl for shutdown trace lines")
         if not env.curl_is_verbose():
@@ -380,9 +346,12 @@ class TestH3ProxyRobustness:
         ]
         assert shutdown_lines, f"No shutdown trace lines found:\n{r.stderr}"
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     def test_60_06_proxy_drop_mid_transfer(self, env: Env, h2o_server, h2o_proxy):
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
 
+        env.make_data_file(indir=h2o_server.docs_dir, fname="proxy-drop-20m", fsize=20 * 1024 * 1024)
         proxy_port = h2o_proxy.port
         url = f"https://localhost:{h2o_server.port}/proxy-drop-20m"
         out_path = os.path.join(env.gen_dir, "proxy-drop.out")
@@ -423,22 +392,13 @@ class TestH3ProxyRobustness:
                 proc.wait(timeout=5)
             assert h2o_proxy.start(), "failed to restart h2o proxy"
 
+    # Large file transfers and multiplexing through HTTP/3 proxy.
 
-class TestH3ProxyDataTransfer:
-    """Large file transfers and multiplexing through HTTP/3 proxy."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
-    @pytest.fixture(autouse=True, scope="class")
-    def _class_scope(self, env, h2o_server):
-        if not env.have_h2o():
-            pytest.skip("h2o not available")
-        env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024)
-        env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024)
-        env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024)
-
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     def test_60_07_large_download(self, env: Env, h2o_server, h2o_proxy):
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
+        env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024)
         curl = CurlClient(env=env)
         url = f"https://localhost:{h2o_server.port}/download-10m"
         proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
@@ -448,8 +408,11 @@ class TestH3ProxyDataTransfer:
         r.check_response(count=1, http_status=200)
         _check_download_size(curl, 10 * 1024 * 1024)
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     def test_60_08_large_upload(self, env: Env, httpd, h2o_server, h2o_proxy):
         _require_available(h2o_proxy=h2o_proxy)
+        env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024)
         fdata = os.path.join(env.gen_dir, "upload-2m")
         curl = CurlClient(env=env)
         url = f"https://localhost:{httpd.ports['https']}/curltest/echo?id=[0-0]"
@@ -463,8 +426,11 @@ class TestH3ProxyDataTransfer:
         )
         r.check_response(count=1, http_status=200)
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     def test_60_09_parallel_downloads(self, env: Env, h2o_server, h2o_proxy):
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
+        env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024)
         count = 5
         curl = CurlClient(env=env)
         urln = f"https://localhost:{h2o_server.port}/download-1m?[0-{count - 1}]"
@@ -475,12 +441,8 @@ class TestH3ProxyDataTransfer:
         )
         r.check_response(count=count, http_status=200)
 
-
-class TestH3ProxyConnectionManagement:
-    """Proxy authentication, connection reuse, and session resumption."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     def test_60_10_proxy_basic_auth(self, env: Env, h2o_server, h2o_proxy):
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
         curl = CurlClient(env=env)
@@ -493,6 +455,8 @@ class TestH3ProxyConnectionManagement:
         r.check_response(count=1, http_status=200)
         _check_download_message(curl, H2O_HELLO_MSG)
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     def test_60_11_connection_reuse(self, env: Env, h2o_server, h2o_proxy):
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
         curl = CurlClient(env=env)
@@ -506,6 +470,8 @@ class TestH3ProxyConnectionManagement:
             f"expected proxy connection reuse, got {r.total_connects} connects"
         )
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     @pytest.mark.skipif(condition=not Env.curl_has_feature('SSLS-EXPORT'),
                         reason='curl lacks SSL session export support')
     def test_60_12_quic_session_resumption(self, env: Env, h2o_server, h2o_proxy):
@@ -530,20 +496,9 @@ class TestH3ProxyConnectionManagement:
         reuses = [line for line in r2.trace_lines if '[SSLS] took session for proxy.http.curl.se' in line]
         assert len(reuses), f'{r2.dump_logs()}'
 
+    # CONNECT-UDP tunnel payload size and capsule-protocol tests.
 
-class TestH3ProxyUdpTunnel:
-    """CONNECT-UDP tunnel payload size and capsule-protocol tests."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS
-
-    @pytest.fixture(autouse=True, scope="class")
-    def _class_scope(self, env, h2o_server):
-        if not env.have_h2o():
-            return
-        env.make_data_file(indir=h2o_server.docs_dir, fname="download-1400", fsize=1400)
-        env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024)
-        env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024)
-
+    @MARK_NEEDS_PROXY_HTTP3
     @MARK_NEEDS_H2O
     @pytest.mark.parametrize(
         "fname,fsize",
@@ -557,6 +512,7 @@ class TestH3ProxyUdpTunnel:
         self, env: Env, h2o_server, h2o_proxy, fname, fsize
     ):
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
+        env.make_data_file(indir=h2o_server.docs_dir, fname=fname, fsize=fsize)
         curl = CurlClient(env=env)
         url = f"https://localhost:{h2o_server.port}/{fname}"
         proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True)
@@ -566,6 +522,7 @@ class TestH3ProxyUdpTunnel:
         r.check_response(count=1, http_status=200)
         _check_download_size(curl, fsize)
 
+    @MARK_NEEDS_PROXY_HTTP3
     @MARK_NEEDS_NGHTTPX
     def test_60_14_udp_tunnel_capsule_absent(
         self, env: Env, httpd, nghttpx, nghttpx_fwd
@@ -585,12 +542,10 @@ class TestH3ProxyUdpTunnel:
             "expected failure: nghttpx does not support CONNECT-UDP / Capsule-Protocol"
         )
 
+    # Timeout and protocol-mismatch edge cases.
 
-class TestH3ProxyEdgeCases:
-    """Timeout and protocol-mismatch edge cases."""
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
+    #@MARK_NEEDS_PROXY_HTTP3
+    #@MARK_NEEDS_H2O
     #def test_60_15_connect_timeout(self, env: Env, h2o_proxy):
     #    _require_available(h2o_proxy=h2o_proxy)
     #    curl = CurlClient(env=env, timeout=15)
@@ -610,6 +565,8 @@ class TestH3ProxyEdgeCases:
     #        f"timeout not respected: took {r.duration.total_seconds():.1f}s"
     #    )
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     @MARK_NEEDS_NGHTTP2
     def test_60_16_h2_uses_connect_tcp_not_udp(self, env: Env, httpd, h2o_proxy):
         _require_available(httpd=httpd, h2o_proxy=h2o_proxy)
@@ -624,18 +581,14 @@ class TestH3ProxyEdgeCases:
         )
         r.check_response(count=1, http_status=200)
 
+    # Verify that happy eyeballs is active for HTTP/3 proxy connections.
+    #
+    # With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address
+    # family selection to the proxy is done by happy eyeballs.
 
-class TestH3ProxyHappyEyeballs:
-    """
-    Verify that happy eyeballs is active for HTTP/3 proxy connections.
-
-    With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address
-    family selection to the proxy is done by happy eyeballs.
-    """
-
-    pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O]
-
-    def test_60_17_h3_proxy_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy):
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
+    def test_60_17_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy):
         """Verbose trace confirms HAPPY-EYEBALLS filter is in the H3 proxy chain."""
         if not env.curl_is_debug():
             pytest.skip("needs debug curl for filter trace")
@@ -651,8 +604,10 @@ class TestH3ProxyHappyEyeballs:
             f"expected HAPPY-EYEBALLS trace for H3 proxy, got: {r.stderr}"
         )
 
+    @MARK_NEEDS_PROXY_HTTP3
+    @MARK_NEEDS_H2O
     @MARK_NEEDS_NGHTTP2
-    def test_60_18_h3_proxy_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy):
+    def test_60_18_happy_eyeballs_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy):
         """IPv4-forced H3 proxy works for h1/h2/h3 inner protocols."""
         _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy)
         for alpn_proto in ["http/1.1", "h2", "h3"]: