Files
gost/tests/e2e/PLAN-HTTP.md
T
ginuerzh 69b561f944 feat: add comprehensive HTTP e2e tests covering connector, TLS, probeResist, idleTimeout, and UDP relay
Adds 12 new HTTP handler test scenarios: HTTP connector (no-auth, auth, TLS
upstream), CONNECT tunnel (no-sniffing, bypass-403), probeResist (host, web,
file, knock), idleTimeout, and UDP relay over HTTP. Introduces
RunGostContainerWithFiles helper for mounting extra files into test containers.
2026-06-26 22:00:35 +08:00

9.7 KiB
Raw Blame History

执行计划:HTTP Handler E2E 测试 + utils.go 重构

背景

从 PLAN.md 的 Step 0 和 Step 1A 开始实施。HTTP proxy 是 gost 最基础的协议,目前 e2e 测试仅覆盖 shadowsocks 和 parallel selector。本计划先重构 utils.go 提取公共 helper,然后用 helper 写 HTTP handler 测试套件,最后同步重构 shadowsocks_test.go。

变更文件清单

文件 操作 说明
tests/e2e/utils.go 修改 新增 RunTCPCase / RunUDPCase
tests/e2e/http_test.go 新建 HTTP proxy 测试套件
tests/e2e/testdata/http/server.yaml 新建 无 auth 的 HTTP proxy 服务器
tests/e2e/testdata/http/server_auth.yaml 新建 带 auth 的 HTTP proxy 服务器
tests/e2e/shadowsocks_test.go 修改 重构为使用 RunTCPCase / RunUDPCase

Step 1: 修改 utils.go — 提取 RunTCPCase / RunUDPCase

shadowsocks_test.gorunTCPCaserunUDPCase 提取为包级函数。关键差异:参数需要传入 echo container IP 和 echo container 本身(用于 dump logs)。

RunTCPCase 签名与逻辑

// RunTCPCase runs a full TCP proxy test case:
// start server → render client config → start client → curl assertion → cleanup.
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP string,
    name, serverConfig, clientConfig string)

逻辑(从 shadowsocks_test.go:132-160 提取,一字不差):

  1. 生成 serverAlias = name + "-server"
  2. RunGostContainerWithOptions(ctx, networkName, serverConfig, [serverAlias], ["8388/tcp"])
  3. defer serverC.Terminate(ctx)
  4. RenderConfig(clientConfig, {ServerAddr: serverAlias + ":8388"})
  5. defer os.Remove(rendered)
  6. RunGostContainerWithPorts(ctx, networkName, rendered, "8080/tcp")
  7. defer clientC.Terminate(ctx)
  8. clientC.Exec(ctx, ["curl", "-v", "-s", "-x", "http://127.0.0.1:8080", "http://<echoIP>:5678"])
  9. 断言 exitCode==0 且 body 包含 "hello-gost",失败时 DumpLogs

问题:端口硬编码。shadowsocks 用 8388 作为服务端口,但其他协议可能用不同端口。需要参数化。

修正设计 — 参数化端口

type TCPOptions struct {
    ServerPort    string // 容器内服务端口,默认 "8388/tcp"
    ClientPort    string // 客户端代理端口,默认 "8080/tcp"
}

func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP string,
    name, serverConfig, clientConfig string, opts ...TCPOptions)

opts 为空时使用默认值 {ServerPort: "8388/tcp", ClientPort: "8080/tcp"}。这样 shadowsocks 测试无需任何改动,而 HTTP 测试可传 TCPOptions{ServerPort: "8080/tcp", ClientPort: "8080/tcp"}

等一下 — 仔细看 shadowsocks 的 server 容器暴露的是 8388/tcpHTTP proxy 场景下只有一个 gost 容器(不做 server→client 链路),直接暴露 proxy 端口。

重新思考:HTTP proxy 不需要 server+client 两容器模式。

HTTP proxy 测试是最简单的模式 — 只需一个 gost 容器运行 HTTP proxy,然后容器内 curl 通过它访问 echo server。这和 parallel_selector_test.go 的模式一致。但为了统一框架和后续协议(SOCKS5、relay 等都需要 server+client 模式),我们应该:

  • HTTP 的"无 auth"用例:单容器模式(同 parallel_selector
  • HTTP 的"带 auth"用例:也可以单容器,只需在 curl 加 --proxy-user

所以 HTTP proxy 不需要 server/client 分离,但 RunTCPCase helper 仍然服务于 SOCKS5/relay 等需要链路的协议。

最终 RunTCPCase 设计

func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP, name string,
    serverConfig, clientConfig string, serverPort, clientPort string)
  • serverPort: 服务端容器暴露端口,如 "8388/tcp"
  • clientPort: 客户端容器暴露端口,如 "8080/tcp"
  • 渲染模板时用 ServerAddr: serverAlias + ":" + strings.TrimSuffix(serverPort, "/tcp")

RunUDPCase 设计

func RunUDPCase(t *testing.T, ctx context.Context, networkName, name string,
    serverConfig, clientConfig string, serverPort, clientPort string)

逻辑从 shadowsocks_test.go:76-126 提取。


Step 2: 新建 testdata/http/ 配置文件

testdata/http/server.yaml — 无 auth 的 HTTP proxy

services:
- name: http-proxy
  addr: :8080
  handler:
    type: http
  listener:
    type: tcp

单容器即可测试:gost 启动后在 :8080 提供 HTTP proxy 服务。

testdata/http/server_auth.yaml — 带 auth 的 HTTP proxy

services:
- name: http-proxy-auth
  addr: :8080
  handler:
    type: http
    auther: auther-0
  listener:
    type: tcp

authers:
- name: auther-0
  auths:
  - username: user
    password: pass

Step 3: 新建 http_test.go

套件结构

type HTTPSuite struct {
    suite.Suite
    ctx    context.Context
    echoC  testcontainers.Container
    echoIP string
}

func (s *HTTPSuite) SetupSuite()    // 启动 TCP echo
func (s *HTTPSuite) TearDownSuite() // 关闭 echo

func (s *HTTPSuite) TestHTTPProxy()       // 无 auth
func (s *HTTPSuite) TestHTTPProxyAuth()   // 带 auth

func TestHTTPSuite(t *testing.T) {
    suite.Run(t, new(HTTPSuite))
}

TestHTTPProxy — 单容器模式(同 parallel_selector

func (s *HTTPSuite) TestHTTPProxy() {
    gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
        "testdata/http/server.yaml", "8080/tcp")
    s.Require().NoError(err)
    defer gostC.Terminate(s.ctx)

    cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
        fmt.Sprintf("http://%s:5678", s.echoIP)}
    code, out, err := gostC.Exec(s.ctx, cmd)
    s.Require().NoError(err)

    body, err := io.ReadAll(out)
    s.Require().NoError(err)
    if code != 0 || !strings.Contains(string(body), "hello-gost") {
        DumpLogs(s.T(), s.ctx, "http-proxy logs", gostC)
    }
    s.Require().Equal(0, code)
    s.Require().Contains(string(body), "hello-gost")
}

TestHTTPProxyAuth — 带 auth 验证

func (s *HTTPSuite) TestHTTPProxyAuth() {
    gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
        "testdata/http/server_auth.yaml", "8080/tcp")
    s.Require().NoError(err)
    defer gostC.Terminate(s.ctx)

    // 测试1:无 auth 应失败(407)
    s.T().Run("no-auth-should-fail", func(t *testing.T) {
        cmd := []string{"curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
            "-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)}
        code, out, _ := gostC.Exec(s.ctx, cmd)
        body, _ := io.ReadAll(out)
        // curl exit code 非 0 或 HTTP status 是 407
        // 注意:gost HTTP proxy 在无 auth 时返回 407curl 不会自动重试
        // 所以这里验证返回码是 407 或 curl 返回非零
        httpStatus := strings.TrimSpace(string(body))
        s.Assert().True(code != 0 || httpStatus == "407",
            "expected auth failure, got status: %s, exit code: %d", httpStatus, code)
    })

    // 测试2:正确 auth 应成功
    s.T().Run("with-auth-should-succeed", func(t *testing.T) {
        cmd := []string{"curl", "-v", "-s", "-x",
            "http://user:pass@127.0.0.1:8080",
            fmt.Sprintf("http://%s:5678", s.echoIP)}
        code, out, err := gostC.Exec(s.ctx, cmd)
        s.Require().NoError(err)

        body, err := io.ReadAll(out)
        s.Require().NoError(err)
        if code != 0 || !strings.Contains(string(body), "hello-gost") {
            DumpLogs(s.T(), s.ctx, "http-proxy-auth logs", gostC)
        }
        s.Require().Equal(0, code)
        s.Require().Contains(string(body), "hello-gost")
    })
}

注意事项

  • HTTP proxy 是单容器模式,不需要 server/client 分离(不像 shadowsocks 需要解密/加密链路)
  • auth 测试分两步:先验证无 auth 被拒(407),再验证正确 auth 成功
  • curl 使用 http://user:pass@host:port 格式传递 proxy auth

Step 4: 重构 shadowsocks_test.go

runTCPCaserunUDPCase 方法替换为调用 utils.go 中的 RunTCPCase / RunUDPCase

Before:

func (s *ShadowsocksSuite) TestShadowsocksTCP() {
    s.runTCPCase("aes256gcm", "testdata/shadowsocks/tcp_server_aes256gcm.yaml", "testdata/shadowsocks/tcp_client_aes256gcm.yaml")
    s.runTCPCase("chacha20", "testdata/shadowsocks/tcp_server_chacha20.yaml", "testdata/shadowsocks/tcp_client_chacha20.yaml")
}

After:

func (s *ShadowsocksSuite) TestShadowsocksTCP() {
    RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "aes256gcm",
        "testdata/shadowsocks/tcp_server_aes256gcm.yaml",
        "testdata/shadowsocks/tcp_client_aes256gcm.yaml",
        "8388/tcp", "8080/tcp")
    RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "chacha20",
        "testdata/shadowsocks/tcp_server_chacha20.yaml",
        "testdata/shadowsocks/tcp_client_chacha20.yaml",
        "8388/tcp", "8080/tcp")
}

删除 runTCPCaserunUDPCase 方法。UDP 测试同理。


实施顺序

  1. utils.go — 添加 RunTCPCase + RunUDPCase(不删除任何现有函数)
  2. testdata/http/ — 创建 server.yaml + server_auth.yaml
  3. http_test.go — 创建 HTTP 套件
  4. 编译验证cd gost && go build ./... && go vet ./tests/e2e/...
  5. shadowsocks_test.go — 重构为使用新 helper
  6. 最终编译验证go build ./... && go vet ./tests/e2e/...
  7. 运行测试go test ./tests/e2e/ -v -run TestHTTPSuite -timeout 5m
  8. 回归测试go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m

验证

cd /config/workspace/go-gost/gost
go build ./...
go vet ./tests/e2e/...
go test ./tests/e2e/ -v -run TestHTTPSuite -timeout 5m
go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m