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.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# 执行计划: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.go` 的 `runTCPCase` 和 `runUDPCase` 提取为包级函数。关键差异:参数需要传入 echo container IP 和 echo container 本身(用于 dump logs)。
|
||||
|
||||
### RunTCPCase 签名与逻辑
|
||||
|
||||
```go
|
||||
// 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 作为服务端口,但其他协议可能用不同端口。需要参数化。
|
||||
|
||||
### 修正设计 — 参数化端口
|
||||
|
||||
```go
|
||||
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/tcp`,HTTP 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 设计
|
||||
|
||||
```go
|
||||
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 设计
|
||||
|
||||
```go
|
||||
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
|
||||
|
||||
```yaml
|
||||
services:
|
||||
- name: http-proxy
|
||||
addr: :8080
|
||||
handler:
|
||||
type: http
|
||||
listener:
|
||||
type: tcp
|
||||
```
|
||||
|
||||
单容器即可测试:gost 启动后在 :8080 提供 HTTP proxy 服务。
|
||||
|
||||
### `testdata/http/server_auth.yaml` — 带 auth 的 HTTP proxy
|
||||
|
||||
```yaml
|
||||
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`
|
||||
|
||||
### 套件结构
|
||||
|
||||
```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)
|
||||
|
||||
```go
|
||||
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 验证
|
||||
|
||||
```go
|
||||
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 时返回 407,curl 不会自动重试
|
||||
// 所以这里验证返回码是 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`
|
||||
|
||||
将 `runTCPCase` 和 `runUDPCase` 方法替换为调用 `utils.go` 中的 `RunTCPCase` / `RunUDPCase`:
|
||||
|
||||
**Before**:
|
||||
```go
|
||||
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**:
|
||||
```go
|
||||
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")
|
||||
}
|
||||
```
|
||||
|
||||
删除 `runTCPCase` 和 `runUDPCase` 方法。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`
|
||||
|
||||
---
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user