- 添加 testutil 容器管理工具(lolly/nginx) - 添加测试配置文件(basic/proxy/ssl) - 添加测试用 SSL 证书 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
256 lines
6.6 KiB
Go
256 lines
6.6 KiB
Go
//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
|
||
}
|
||
|
||
// StartNginxContainer 启动 nginx 容器,返回容器和访问地址。
|
||
func StartNginxContainer(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 nginx container: %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
|
||
}
|