diff --git a/tests/http/test_60_h3_proxy.py b/tests/http/test_60_h3_proxy.py index 4d19981650..7ebba8a2b1 100644 --- a/tests/http/test_60_h3_proxy.py +++ b/tests/http/test_60_h3_proxy.py @@ -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()}" ) + # Guard checks for unsupported HTTP/3 proxy options. -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", - ), - ] - - @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"]: