pytest: fixes and tidy-ups to h3-proxy tests

- 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 e4139a73c8 #21798
- avoid creating unnecessary test data blobs, and minimize their scopes.

Follow-up to 91facd7bb3 #21791
Follow-up to e78b1b3ecc #21153

Closes #21811
This commit is contained in:
Viktor Szakats 2026-05-30 10:53:21 +02:00
parent 872c313d76
commit d806323ffd
No known key found for this signature in database

View 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()}"
)
# 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"]: