lolly/internal/e2e/testutil/container.go
xfy 76ab9bcb24 refactor(e2e): 统一 E2E 测试以 lolly 为被测系统
- 将 StartNginxContainer 重命名为 StartMockBackend,明确其作为模拟后端的用途
- proxy_e2e_test.go: 所有测试使用 lolly 作为代理,nginx 作为后端
- ssl_e2e_test.go: 移除 nginx 容器测试,简化为 lolly SSL 功能测试
- static_e2e_test.go: 所有测试使用 lolly 作为静态文件服务器

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 10:25:56 +08:00

259 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build e2e
// Package testutil 提供 E2E 测试的工具函数。
//
// 包含容器管理、测试配置、等待工具等。
//
// 作者xfy
package testutil
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
// defaultLollyConfig 是 lolly 容器的默认配置。
const defaultLollyConfig = `
servers:
- listen: ":8080"
static:
- path: "/"
root: "/var/www/html"
index:
- "index.html"
`
// LollyContainer 封装 lolly 服务器容器。
type LollyContainer struct {
Container testcontainers.Container
Host string
HTTPPort int
HTTPSPort int
}
// StartLollyContainer 启动 lolly 服务器容器。
//
// 使用预构建的 lolly 镜像。如果 configPath 为空,使用默认配置。
func StartLollyContainer(ctx context.Context, configPath string) (*LollyContainer, error) {
req := testcontainers.ContainerRequest{
Image: "lolly:latest",
ExposedPorts: []string{"8080/tcp", "8443/tcp"},
WaitingFor: wait.ForLog("HTTP 服务器启动中").WithStartupTimeout(30 * time.Second),
}
// 配置文件挂载
if configPath != "" {
req.Mounts = []testcontainers.ContainerMount{
{
Source: testcontainers.GenericBindMountSource{
HostPath: configPath,
},
Target: "/etc/lolly/lolly.yaml",
},
}
} else {
// 使用内嵌默认配置
req.Files = []testcontainers.ContainerFile{
{
Reader: strings.NewReader(defaultLollyConfig),
ContainerFilePath: "/etc/lolly/lolly.yaml",
FileMode: 0o644,
},
}
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, fmt.Errorf("failed to start container: %w", err)
}
host, err := container.Host(ctx)
if err != nil {
container.Terminate(ctx)
return nil, fmt.Errorf("failed to get host: %w", err)
}
httpPort, err := container.MappedPort(ctx, "8080/tcp")
if err != nil {
container.Terminate(ctx)
return nil, fmt.Errorf("failed to get HTTP port: %w", err)
}
httpsPort, err := container.MappedPort(ctx, "8443/tcp")
if err != nil {
container.Terminate(ctx)
return nil, fmt.Errorf("failed to get HTTPS port: %w", err)
}
// 解析端口数字
httpPortInt := parsePort(httpPort.Port())
httpsPortInt := parsePort(httpsPort.Port())
return &LollyContainer{
Container: container,
Host: host,
HTTPPort: httpPortInt,
HTTPSPort: httpsPortInt,
}, nil
}
// parsePort 解析端口字符串为整数。
func parsePort(portStr string) int {
var port int
fmt.Sscanf(portStr, "%d", &port)
return port
}
// HTTPBaseURL 返回 HTTP 基础 URL。
func (c *LollyContainer) HTTPBaseURL() string {
return fmt.Sprintf("http://%s:%d", c.Host, c.HTTPPort)
}
// HTTPSBaseURL 返回 HTTPS 基础 URL。
func (c *LollyContainer) HTTPSBaseURL() string {
return fmt.Sprintf("https://%s:%d", c.Host, c.HTTPSPort)
}
// Terminate 终止容器。
func (c *LollyContainer) Terminate(ctx context.Context) error {
if c.Container != nil {
return c.Container.Terminate(ctx)
}
return nil
}
// WaitForHealthy 等待服务健康。
func (c *LollyContainer) WaitForHealthy(ctx context.Context, timeout time.Duration) error {
client := &http.Client{Timeout: 5 * time.Second}
url := c.HTTPBaseURL()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
resp, err := client.Get(url)
if err == nil {
resp.Body.Close()
if resp.StatusCode == 200 || resp.StatusCode == 404 {
// 200 或 404 都表示服务器正在运行
return nil
}
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("service not healthy after %v", timeout)
}
// MockBackendContainer 启动一个模拟后端服务器容器。
func MockBackendContainer(ctx context.Context, port int) (testcontainers.Container, string, error) {
req := testcontainers.ContainerRequest{
Image: "nginx:alpine",
ExposedPorts: []string{fmt.Sprintf("%d/tcp", port)},
WaitingFor: wait.ForHTTP("/").WithPort(fmt.Sprintf("%d/tcp", port)).WithStartupTimeout(30 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, "", fmt.Errorf("failed to start mock backend: %w", err)
}
host, err := container.Host(ctx)
if err != nil {
container.Terminate(ctx)
return nil, "", fmt.Errorf("failed to get host: %w", err)
}
mappedPort, err := container.MappedPort(ctx, fmt.Sprintf("%d/tcp", port))
if err != nil {
container.Terminate(ctx)
return nil, "", fmt.Errorf("failed to get port: %w", err)
}
addr := fmt.Sprintf("http://%s:%s", host, mappedPort.Port())
return container, addr, nil
}
// DockerAvailable 检查 Docker 是否可用。
func DockerAvailable(ctx context.Context) bool {
req := testcontainers.ContainerRequest{
Image: "alpine:latest",
Cmd: []string{"echo", "test"},
AutoRemove: true,
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return false
}
container.Terminate(ctx)
return true
}
// LollyImageAvailable 检查 lolly:latest 镜像是否可用。
func LollyImageAvailable(ctx context.Context) bool {
req := testcontainers.ContainerRequest{
Image: "lolly:latest",
Cmd: []string{"echo", "test"},
AutoRemove: true,
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return false
}
container.Terminate(ctx)
return true
}
// StartMockBackend 启动模拟后端容器(用于代理测试)。
//
// 使用 nginx 作为模拟后端,返回容器和访问地址。
// 注意:此函数仅用于代理测试的后端模拟,不应作为被测系统。
func StartMockBackend(ctx context.Context) (testcontainers.Container, string, error) {
req := testcontainers.ContainerRequest{
Image: "nginx:alpine",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(30 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, "", fmt.Errorf("failed to start mock backend: %w", err)
}
host, err := container.Host(ctx)
if err != nil {
container.Terminate(ctx)
return nil, "", fmt.Errorf("failed to get host: %w", err)
}
port, err := container.MappedPort(ctx, "80/tcp")
if err != nil {
container.Terminate(ctx)
return nil, "", fmt.Errorf("failed to get port: %w", err)
}
addr := fmt.Sprintf("http://%s:%s", host, port.Port())
return container, addr, nil
}