test(http2): 添加 HTTP/2 E2E 和 TLS 集成测试
- 添加 HTTP/2 协议协商、流多路复用、头部压缩 E2E 测试 - 添加 TLS 握手、ALPN 协商、HTTP/1.1 回退集成测试 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2ffcfd782b
commit
e145f1b080
476
internal/e2e/http2_e2e_test.go
Normal file
476
internal/e2e/http2_e2e_test.go
Normal file
@ -0,0 +1,476 @@
|
||||
//go:build e2e
|
||||
|
||||
// http2_e2e_test.go - HTTP/2 协议 E2E 测试
|
||||
//
|
||||
// 测试 lolly HTTP/2 功能:协议协商、流多路复用、头部压缩等。
|
||||
//
|
||||
// 作者:xfy
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"rua.plus/lolly/internal/e2e/testutil"
|
||||
)
|
||||
|
||||
// TestE2EHTTP2ProtocolNegotiation 测试 HTTP/2 协议协商。
|
||||
//
|
||||
// 验证 ALPN 协商成功选择 h2 协议。
|
||||
func TestE2EHTTP2ProtocolNegotiation(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建带 SSL 的配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithStatic("/", "/var/www/html")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 创建 HTTP/2 客户端
|
||||
client := &http.Client{
|
||||
Transport: &http2.Transport{
|
||||
AllowHTTP: false,
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, *tls.ConnectionState, error) {
|
||||
dialer := &net.Dialer{}
|
||||
conn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tlsConn := tls.Client(conn, cfg)
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
return tlsConn, &tlsConn.ConnectionState{}, nil
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Get(lolly.HTTPSBaseURL())
|
||||
require.NoError(t, err, "HTTP/2 request failed")
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证 HTTP/2 协议
|
||||
assert.Equal(t, 2, resp.ProtoMajor, "Expected HTTP/2 protocol")
|
||||
t.Logf("HTTP/2 negotiation successful, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// TestE2EHTTP2StreamMultiplexing 测试 HTTP/2 流多路复用。
|
||||
//
|
||||
// 验证多个并发请求在单个连接上复用。
|
||||
func TestE2EHTTP2StreamMultiplexing(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithStatic("/", "/var/www/html")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 创建 HTTP/2 客户端
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
transport := &http2.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// 并发发送多个请求
|
||||
numRequests := 10
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, numRequests)
|
||||
start := time.Now()
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
resp, err := client.Get(fmt.Sprintf("%s/test%d", lolly.HTTPSBaseURL(), id))
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("request %d failed: %w", id, err)
|
||||
return
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
elapsed := time.Since(start)
|
||||
t.Logf("Completed %d requests in %v", numRequests, elapsed)
|
||||
|
||||
// 检查错误
|
||||
for err := range errors {
|
||||
t.Errorf("Request error: %v", err)
|
||||
}
|
||||
|
||||
// 多路复用应该比串行请求快
|
||||
// 如果每个请求需要 100ms,串行需要 1s,多路复用应该更快
|
||||
assert.Less(t, elapsed, 2*time.Second, "Multiplexed requests should complete quickly")
|
||||
}
|
||||
|
||||
// TestE2EHTTP2HeaderCompression 测试 HTTP/2 头部压缩。
|
||||
//
|
||||
// 验证 HPACK 压缩正常工作。
|
||||
func TestE2EHTTP2HeaderCompression(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithStatic("/", "/var/www/html")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 创建 HTTP/2 客户端
|
||||
transport := &http2.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// 发送多个请求,头部应该被压缩复用
|
||||
for i := 0; i < 5; i++ {
|
||||
req, err := http.NewRequest("GET", lolly.HTTPSBaseURL(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 添加自定义头部
|
||||
req.Header.Set("X-Custom-Header", "test-value-that-should-be-compressed")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Logf("Request %d error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
t.Logf("Request %d: status %d", i, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EHTTP2ServerPush 测试 HTTP/2 服务器推送(如果支持)。
|
||||
//
|
||||
// 验证服务器推送功能。
|
||||
func TestE2EHTTP2ServerPush(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithStatic("/", "/var/www/html")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 创建支持推送的客户端
|
||||
transport := &http2.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Get(lolly.HTTPSBaseURL())
|
||||
require.NoError(t, err, "Request failed")
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查是否支持推送(通过响应头)
|
||||
pushSupported := resp.Header.Get("HTTP2-Settings") != ""
|
||||
t.Logf("HTTP/2 response status: %d, push supported: %v", resp.StatusCode, pushSupported)
|
||||
}
|
||||
|
||||
// TestE2EHTTP2ConnectionPreface 测试 HTTP/2 连接前缀。
|
||||
//
|
||||
// 验证服务器正确响应 HTTP/2 连接前缀。
|
||||
func TestE2EHTTP2ConnectionPreface(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithStatic("/", "/var/www/html")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 建立 TLS 连接
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", lolly.HTTPSAddr())
|
||||
require.NoError(t, err, "Failed to connect")
|
||||
defer conn.Close()
|
||||
|
||||
tlsConn := tls.Client(conn, &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h2"},
|
||||
})
|
||||
require.NoError(t, tlsConn.HandshakeContext(ctx), "TLS handshake failed")
|
||||
|
||||
// 验证协商的协议
|
||||
state := tlsConn.ConnectionState()
|
||||
assert.Equal(t, "h2", state.NegotiatedProtocol, "Expected h2 protocol")
|
||||
|
||||
// 发送 HTTP/2 连接前缀
|
||||
preface := "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
||||
_, err = tlsConn.Write([]byte(preface))
|
||||
require.NoError(t, err, "Failed to send preface")
|
||||
|
||||
// 读取响应(服务器应该发送 SETTINGS 帧)
|
||||
buf := make([]byte, 1024)
|
||||
_ = tlsConn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
n, err := tlsConn.Read(buf)
|
||||
if err != nil {
|
||||
t.Logf("Read error (expected SETTINGS frame): %v", err)
|
||||
} else {
|
||||
t.Logf("Received %d bytes after preface", n)
|
||||
// 检查是否是 SETTINGS 帧(类型 0x04)
|
||||
if n >= 9 && buf[3] == 0x04 {
|
||||
t.Log("Received SETTINGS frame")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EHTTP2LargeRequest 测试 HTTP/2 大请求处理。
|
||||
//
|
||||
// 验证大请求体的处理。
|
||||
func TestE2EHTTP2LargeRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithProxy("/upload", "http://backend:8080")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 创建 HTTP/2 客户端
|
||||
transport := &http2.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// 发送大请求(1MB)
|
||||
largeBody := strings.NewReader(strings.Repeat("x", 1024*1024))
|
||||
req, err := http.NewRequest("POST", lolly.HTTPSBaseURL()+"/upload", largeBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Logf("Large request error: %v", err)
|
||||
} else {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
t.Logf("Large request status: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EHTTP2ConcurrentStreams 测试 HTTP/2 并发流限制。
|
||||
//
|
||||
// 验证服务器正确处理大量并发流。
|
||||
func TestE2EHTTP2ConcurrentStreams(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.DefaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !testutil.LollyImageAvailable(ctx) {
|
||||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||||
}
|
||||
|
||||
// 生成自签名证书
|
||||
certPath, keyPath, cleanup, err := testutil.GenerateSelfSignedCert(t.TempDir())
|
||||
require.NoError(t, err, "Failed to generate certificate")
|
||||
defer cleanup()
|
||||
|
||||
// 构建配置
|
||||
cfg := testutil.NewConfigBuilder().
|
||||
WithServer(":8443").
|
||||
WithSSL("/etc/lolly/ssl/server.crt", "/etc/lolly/ssl/server.key").
|
||||
WithStatic("/", "/var/www/html")
|
||||
|
||||
configYAML, err := cfg.Build()
|
||||
require.NoError(t, err, "Failed to build config")
|
||||
|
||||
// 启动 lolly
|
||||
lolly, err := testutil.StartLolly(ctx,
|
||||
testutil.WithConfigYAML(configYAML),
|
||||
testutil.WithCert(certPath, keyPath),
|
||||
)
|
||||
require.NoError(t, err, "Failed to start lolly")
|
||||
defer lolly.Terminate(ctx)
|
||||
|
||||
// 创建 HTTP/2 客户端
|
||||
transport := &http2.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// 发送大量并发请求
|
||||
numStreams := 100
|
||||
var wg sync.WaitGroup
|
||||
successCount := 0
|
||||
mu := sync.Mutex{}
|
||||
|
||||
for i := 0; i < numStreams; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
resp, err := client.Get(fmt.Sprintf("%s/stream%d", lolly.HTTPSBaseURL(), id))
|
||||
if err == nil {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
mu.Lock()
|
||||
successCount++
|
||||
mu.Unlock()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Successfully handled %d/%d concurrent streams", successCount, numStreams)
|
||||
assert.Greater(t, successCount, numStreams/2, "Most streams should succeed")
|
||||
}
|
||||
687
internal/http2/integration_tls_test.go
Normal file
687
internal/http2/integration_tls_test.go
Normal file
@ -0,0 +1,687 @@
|
||||
// Package http2 提供 HTTP/2 TLS 连接集成测试。
|
||||
//
|
||||
// 该文件测试 HTTP/2 服务器的 TLS 相关功能:
|
||||
// - TLS 握手成功/失败
|
||||
// - ALPN 协商 h2/http1.1
|
||||
// - HTTP/1.1 回退路径
|
||||
//
|
||||
// 作者:xfy
|
||||
package http2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"rua.plus/lolly/internal/config"
|
||||
)
|
||||
|
||||
// generateTestCert 生成测试用自签名证书。
|
||||
func generateTestCert(t *testing.T) (tls.Certificate, *x509.CertPool) {
|
||||
t.Helper()
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test Org"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
cert := tls.Certificate{
|
||||
Certificate: [][]byte{certDER},
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AppendCertsFromPEM(certDER)
|
||||
|
||||
return cert, certPool
|
||||
}
|
||||
|
||||
// TestTLSHandshakeSuccess 测试 TLS 握手成功。
|
||||
func TestTLSHandshakeSuccess(t *testing.T) {
|
||||
cert, _ := generateTestCert(t)
|
||||
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.WriteString("Hello HTTP/2")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
}
|
||||
|
||||
cfg := &config.HTTP2Config{
|
||||
Enabled: true,
|
||||
MaxConcurrentStreams: 100,
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, handler, tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
|
||||
// 创建管道连接
|
||||
serverConn, clientConn := net.Pipe()
|
||||
defer func() {
|
||||
_ = serverConn.Close()
|
||||
_ = clientConn.Close()
|
||||
}()
|
||||
|
||||
// 包装服务器端连接为 TLS
|
||||
tlsServerConn := tls.Server(serverConn, tlsConfig)
|
||||
|
||||
// 需要先 Add(1) 因为 handleConnection 会调用 Done()
|
||||
server.connWg.Add(1)
|
||||
|
||||
// 在后台处理连接
|
||||
go func() {
|
||||
server.handleConnection(tlsServerConn)
|
||||
}()
|
||||
|
||||
// 客户端 TLS 握手
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
tlsClientConn := tls.Client(clientConn, tlsClientConfig)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := tlsClientConn.HandshakeContext(ctx); err != nil {
|
||||
t.Fatalf("TLS handshake failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证协商的协议
|
||||
state := tlsClientConn.ConnectionState()
|
||||
if state.NegotiatedProtocol != "h2" {
|
||||
t.Errorf("Expected negotiated protocol 'h2', got '%s'", state.NegotiatedProtocol)
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
_ = tlsClientConn.Close()
|
||||
_ = tlsServerConn.Close()
|
||||
|
||||
// 等待处理完成
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
server.connWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
t.Log("Connection handling completed")
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Log("Timeout waiting for connection handling")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLSHandshakeFailure 测试 TLS 握手失败。
|
||||
func TestTLSHandshakeFailure(t *testing.T) {
|
||||
cert, _ := generateTestCert(t)
|
||||
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.WriteString("Hello")
|
||||
}
|
||||
|
||||
cfg := &config.HTTP2Config{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, handler, tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
|
||||
// 包装服务器端连接为 TLS
|
||||
tlsServerConn := tls.Server(serverConn, tlsConfig)
|
||||
|
||||
// 需要先 Add(1) 因为 handleConnection 会调用 Done()
|
||||
server.connWg.Add(1)
|
||||
|
||||
// 在后台处理连接
|
||||
go func() {
|
||||
server.handleConnection(tlsServerConn)
|
||||
}()
|
||||
|
||||
// 客户端不进行 TLS 握手,直接发送无效数据
|
||||
_, _ = clientConn.Write([]byte("INVALID DATA NOT TLS"))
|
||||
|
||||
// 等待处理完成
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 关闭连接
|
||||
_ = clientConn.Close()
|
||||
_ = tlsServerConn.Close()
|
||||
|
||||
// 等待处理完成
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
server.connWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
t.Log("Connection handling completed after handshake failure")
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Log("Timeout waiting for connection handling")
|
||||
}
|
||||
}
|
||||
|
||||
// TestALPNNegotiationH2 测试 ALPN 协商选择 h2。
|
||||
func TestALPNNegotiationH2(t *testing.T) {
|
||||
cert, _ := generateTestCert(t)
|
||||
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.WriteString("OK")
|
||||
}
|
||||
|
||||
cfg := &config.HTTP2Config{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, handler, tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
|
||||
// 验证 ALPN 配置
|
||||
alpnConfig := server.ALPNConfig()
|
||||
if alpnConfig == nil {
|
||||
t.Fatal("ALPN config should not be nil")
|
||||
}
|
||||
|
||||
foundH2 := false
|
||||
for _, proto := range alpnConfig.NextProtos {
|
||||
if proto == "h2" {
|
||||
foundH2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundH2 {
|
||||
t.Error("ALPN config should include h2 protocol")
|
||||
}
|
||||
}
|
||||
|
||||
// TestALPNHTTP11Fallback 测试 ALPN 协商回退到 HTTP/1.1。
|
||||
func TestALPNHTTP11Fallback(t *testing.T) {
|
||||
cert, _ := generateTestCert(t)
|
||||
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.WriteString("HTTP/1.1 response")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
}
|
||||
|
||||
cfg := &config.HTTP2Config{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, handler, tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
defer func() {
|
||||
_ = serverConn.Close()
|
||||
_ = clientConn.Close()
|
||||
}()
|
||||
|
||||
tlsServerConn := tls.Server(serverConn, tlsConfig)
|
||||
|
||||
// 需要先 Add(1) 因为 handleConnection 会调用 Done()
|
||||
server.connWg.Add(1)
|
||||
|
||||
go func() {
|
||||
server.handleConnection(tlsServerConn)
|
||||
}()
|
||||
|
||||
// 客户端只支持 HTTP/1.1
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
tlsClientConn := tls.Client(clientConn, tlsClientConfig)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := tlsClientConn.HandshakeContext(ctx); err != nil {
|
||||
t.Fatalf("TLS handshake failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证协商的协议是 http/1.1
|
||||
state := tlsClientConn.ConnectionState()
|
||||
if state.NegotiatedProtocol != "http/1.1" {
|
||||
t.Errorf("Expected negotiated protocol 'http/1.1', got '%s'", state.NegotiatedProtocol)
|
||||
}
|
||||
|
||||
_ = tlsClientConn.Close()
|
||||
_ = tlsServerConn.Close()
|
||||
}
|
||||
|
||||
// TestTLSListenerWrapper 测试 TLS 监听器包装。
|
||||
func TestTLSListenerWrapper(t *testing.T) {
|
||||
cert, _ := generateTestCert(t)
|
||||
|
||||
// 创建底层监听器
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener: %v", err)
|
||||
}
|
||||
defer func() { _ = ln.Close() }()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
// 包装监听器
|
||||
wrappedLn := WrapTLSListener(ln, tlsConfig)
|
||||
if wrappedLn == nil {
|
||||
t.Fatal("WrapTLSListener returned nil")
|
||||
}
|
||||
|
||||
// 验证 NextProtos 已设置
|
||||
if len(tlsConfig.NextProtos) == 0 {
|
||||
t.Error("NextProtos should be set after wrapping")
|
||||
}
|
||||
|
||||
foundH2 := false
|
||||
foundHTTP11 := false
|
||||
for _, proto := range tlsConfig.NextProtos {
|
||||
if proto == "h2" {
|
||||
foundH2 = true
|
||||
}
|
||||
if proto == "http/1.1" {
|
||||
foundHTTP11 = true
|
||||
}
|
||||
}
|
||||
if !foundH2 || !foundHTTP11 {
|
||||
t.Error("NextProtos should include both h2 and http/1.1")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTLSListenerExistingProtos 测试已有 NextProtos 的情况。
|
||||
func TestTLSListenerExistingProtos(t *testing.T) {
|
||||
cert, _ := generateTestCert(t)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener: %v", err)
|
||||
}
|
||||
defer func() { _ = ln.Close() }()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"custom-proto"},
|
||||
}
|
||||
|
||||
wrappedLn := WrapTLSListener(ln, tlsConfig)
|
||||
if wrappedLn == nil {
|
||||
t.Fatal("WrapTLSListener returned nil")
|
||||
}
|
||||
|
||||
// 已有 NextProtos 不应被覆盖
|
||||
if len(tlsConfig.NextProtos) != 1 || tlsConfig.NextProtos[0] != "custom-proto" {
|
||||
t.Errorf("Existing NextProtos should not be overwritten, got %v", tlsConfig.NextProtos)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeHTTP1Fallback 测试 HTTP/1.1 回退。
|
||||
func TestServeHTTP1Fallback(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.WriteString("HTTP/1.1 response")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
ctx.Response.Header.Set("X-Test", "value")
|
||||
}
|
||||
|
||||
cfg := &config.HTTP2Config{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, handler, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server: %v", err)
|
||||
}
|
||||
|
||||
serverConn, clientConn := net.Pipe()
|
||||
defer func() {
|
||||
_ = serverConn.Close()
|
||||
_ = clientConn.Close()
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
server.serveHTTP1(serverConn)
|
||||
}()
|
||||
|
||||
// 发送 HTTP/1.1 请求
|
||||
request := "GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
||||
_, _ = clientConn.Write([]byte(request))
|
||||
|
||||
// 读取响应
|
||||
buf := make([]byte, 1024)
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
n, err := clientConn.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response: %v", err)
|
||||
}
|
||||
|
||||
response := string(buf[:n])
|
||||
if response == "" {
|
||||
t.Error("Expected non-empty response")
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
_ = clientConn.Close()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestConnectionPoolOperations 测试连接池操作。
|
||||
func TestConnectionPoolOperations(t *testing.T) {
|
||||
pool := newConnectionPool()
|
||||
|
||||
// 创建模拟连接
|
||||
conn1 := &mockTestConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}}
|
||||
conn2 := &mockTestConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12346}}
|
||||
|
||||
// 添加连接
|
||||
pool.add("client1", conn1)
|
||||
pool.add("client1", conn2)
|
||||
|
||||
// 验证连接数
|
||||
if count := pool.count("client1"); count != 2 {
|
||||
t.Errorf("Expected 2 connections, got %d", count)
|
||||
}
|
||||
|
||||
// 获取连接
|
||||
conns := pool.get("client1")
|
||||
if len(conns) != 2 {
|
||||
t.Errorf("Expected 2 connections, got %d", len(conns))
|
||||
}
|
||||
|
||||
// 移除连接
|
||||
pool.remove("client1", conn1)
|
||||
if count := pool.count("client1"); count != 1 {
|
||||
t.Errorf("Expected 1 connection after removal, got %d", count)
|
||||
}
|
||||
|
||||
// 关闭所有连接
|
||||
pool.closeAll()
|
||||
if count := pool.count("client1"); count != 0 {
|
||||
t.Errorf("Expected 0 connections after closeAll, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// mockTestConn 是用于测试的模拟连接。
|
||||
type mockTestConn struct {
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func (m *mockTestConn) Read(_ []byte) (n int, err error) { return 0, nil }
|
||||
func (m *mockTestConn) Write(_ []byte) (n int, err error) { return 0, nil }
|
||||
func (m *mockTestConn) Close() error { return nil }
|
||||
func (m *mockTestConn) LocalAddr() net.Addr { return m.remoteAddr }
|
||||
func (m *mockTestConn) RemoteAddr() net.Addr { return m.remoteAddr }
|
||||
func (m *mockTestConn) SetDeadline(_ time.Time) error { return nil }
|
||||
func (m *mockTestConn) SetReadDeadline(_ time.Time) error { return nil }
|
||||
func (m *mockTestConn) SetWriteDeadline(_ time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestIsHTTP2RequestMethod 测试 HTTP/2 请求检测。
|
||||
func TestIsHTTP2RequestMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
proto int
|
||||
want bool
|
||||
hasPseudoHeader bool
|
||||
}{
|
||||
{"PRI method", "PRI", 1, true, false},
|
||||
{"HTTP/2 version", "GET", 2, true, false},
|
||||
{"HTTP/1.1", "GET", 1, false, false},
|
||||
{"With pseudo header", "GET", 1, true, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(tt.method, "http://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
if tt.proto == 2 {
|
||||
req.ProtoMajor = 2
|
||||
}
|
||||
if tt.hasPseudoHeader {
|
||||
req.Header.Set(":method", "GET")
|
||||
}
|
||||
|
||||
if got := IsHTTP2Request(req); got != tt.want {
|
||||
t.Errorf("IsHTTP2Request() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetALPNProtocolNonTLS 测试获取 ALPN 协议(非 TLS)。
|
||||
func TestGetALPNProtocolNonTLS(t *testing.T) {
|
||||
// 非 TLS 连接
|
||||
plainConn := &mockTestConn{}
|
||||
if proto := GetALPNProtocol(plainConn); proto != "" {
|
||||
t.Errorf("Expected empty protocol for non-TLS connection, got '%s'", proto)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateSettingsFunc 测试设置验证。
|
||||
func TestValidateSettingsFunc(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
settings Settings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid settings",
|
||||
settings: DefaultSettings(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "zero max concurrent streams",
|
||||
settings: Settings{
|
||||
MaxConcurrentStreams: 0,
|
||||
MaxFrameSize: 16384,
|
||||
MaxHeaderListSize: 4096,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid max frame size - too small",
|
||||
settings: Settings{
|
||||
MaxConcurrentStreams: 100,
|
||||
MaxFrameSize: 1000,
|
||||
MaxHeaderListSize: 4096,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid max frame size - too large",
|
||||
settings: Settings{
|
||||
MaxConcurrentStreams: 100,
|
||||
MaxFrameSize: 20000000,
|
||||
MaxHeaderListSize: 4096,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid initial window size",
|
||||
settings: Settings{
|
||||
MaxConcurrentStreams: 100,
|
||||
MaxFrameSize: 16384,
|
||||
InitialWindowSize: 3000000000,
|
||||
MaxHeaderListSize: 4096,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "zero max header list size",
|
||||
settings: Settings{
|
||||
MaxConcurrentStreams: 100,
|
||||
MaxFrameSize: 16384,
|
||||
MaxHeaderListSize: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSettings(tt.settings)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateSettings() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseSettingsFunc 测试设置解析。
|
||||
func TestParseSettingsFunc(t *testing.T) {
|
||||
cfg := &config.HTTP2Config{
|
||||
MaxConcurrentStreams: 200,
|
||||
MaxHeaderListSize: 2048576,
|
||||
PushEnabled: false,
|
||||
}
|
||||
|
||||
settings := ParseSettings(cfg)
|
||||
|
||||
if settings.MaxConcurrentStreams != 200 {
|
||||
t.Errorf("Expected MaxConcurrentStreams 200, got %d", settings.MaxConcurrentStreams)
|
||||
}
|
||||
if settings.MaxHeaderListSize != 2048576 {
|
||||
t.Errorf("Expected MaxHeaderListSize 2048576, got %d", settings.MaxHeaderListSize)
|
||||
}
|
||||
if settings.EnablePush {
|
||||
t.Error("Expected EnablePush to be false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultSettingsFunc 测试默认设置。
|
||||
func TestDefaultSettingsFunc(t *testing.T) {
|
||||
settings := DefaultSettings()
|
||||
|
||||
if settings.HeaderTableSize != 4096 {
|
||||
t.Errorf("Expected HeaderTableSize 4096, got %d", settings.HeaderTableSize)
|
||||
}
|
||||
if !settings.EnablePush {
|
||||
t.Error("Expected EnablePush to be true")
|
||||
}
|
||||
if settings.MaxConcurrentStreams != 250 {
|
||||
t.Errorf("Expected MaxConcurrentStreams 250, got %d", settings.MaxConcurrentStreams)
|
||||
}
|
||||
if settings.InitialWindowSize != 65535 {
|
||||
t.Errorf("Expected InitialWindowSize 65535, got %d", settings.InitialWindowSize)
|
||||
}
|
||||
if settings.MaxFrameSize != 16384 {
|
||||
t.Errorf("Expected MaxFrameSize 16384, got %d", settings.MaxFrameSize)
|
||||
}
|
||||
if settings.MaxHeaderListSize != 1048576 {
|
||||
t.Errorf("Expected MaxHeaderListSize 1048576, got %d", settings.MaxHeaderListSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupportsHTTP2Func 测试 HTTP/2 支持检测。
|
||||
func TestSupportsHTTP2Func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupReq func(*http.Request)
|
||||
wantResult bool
|
||||
}{
|
||||
{
|
||||
name: "HTTP/2 request",
|
||||
setupReq: func(r *http.Request) {
|
||||
r.ProtoMajor = 2
|
||||
},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "h2c upgrade",
|
||||
setupReq: func(r *http.Request) {
|
||||
r.Header.Set("Upgrade", "h2c")
|
||||
},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP2-Settings header",
|
||||
setupReq: func(r *http.Request) {
|
||||
r.Header.Set("HTTP2-Settings", "some-settings")
|
||||
},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP/1.1 without upgrade",
|
||||
setupReq: func(r *http.Request) {},
|
||||
wantResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
tt.setupReq(req)
|
||||
|
||||
if got := SupportsHTTP2(req); got != tt.wantResult {
|
||||
t.Errorf("SupportsHTTP2() = %v, want %v", got, tt.wantResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user