test(e2e): 添加 E2E 测试工具包和配置
- 添加 testutil 容器管理工具(lolly/nginx) - 添加测试配置文件(basic/proxy/ssl) - 添加测试用 SSL 证书 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8f79fb6797
commit
5d38d9ab44
19
internal/e2e/certs/server.crt
Normal file
19
internal/e2e/certs/server.crt
Normal file
@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCTCCAfGgAwIBAgIUAJotgH8D4lGjfdPM5lLNvMPrwRowDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDQyMjEwNDQ1MloXDTI3MDQy
|
||||
MjEwNDQ1MlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAy/raK3YKoM7dOrQqV11ADOVnhh4j214RO6N5wWty7JAI
|
||||
b+Yr464ZiOBTwinyCMFo47Obq5+RRCZmHS8gkrcLMv4yiMLU87ZGK29w5qlDhUVL
|
||||
75w26qCGnCoxCS4zcicOpI+zh/siDMEVh/DG8SAzhdEZkgFyFsCigIwCGtdL//RF
|
||||
iIyp/WYHmsqmyTS+03MBOmzHT+jDS5Ilgt/651VaLArIjnMEGQVhQEG7IMfdq3yO
|
||||
SZZBsJtB4EECA5TuPIgBbCUuG8dZCnhJAQ3C9cKt9HJ0UnRhpJmWWrO1WmTA7WFb
|
||||
izfE28VA6gsei+aAKFJ/ti/O0h8leq00p1unJ6vCswIDAQABo1MwUTAdBgNVHQ4E
|
||||
FgQUynDeX1x5o5rZK4wvNAqZRVaBIe4wHwYDVR0jBBgwFoAUynDeX1x5o5rZK4wv
|
||||
NAqZRVaBIe4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAk/zR
|
||||
fBVCpJUVcLeXqHlBaiWPkvWk2UG5zHjUOQy9oq9JyCnbRTXYr1eWGYc6Ycc66n8i
|
||||
+gAj9TURBhjqwxy+bseSr9XRlyVDzExx1K57ETXE00OakLlFhga9RkOSQeOoVZfo
|
||||
pkMTOpYOeFW2S6Oz6yYo+vOxnFwAajuio1AD1rkVCKGFDj3LJry1frz1WURYsMvA
|
||||
AwnAHva4GL4cd1xbCedpZiLRO03RjluU3peGi6zhZDWNLWoBS3fxRFZBkG+1D2pD
|
||||
lPn43V2sysV25/8LYbxWN9CUiAlTDvFaykiOVO2RSAz84cxyNGb1JsE2059XqXXQ
|
||||
sMajslnTT8QJASm+cg==
|
||||
-----END CERTIFICATE-----
|
||||
28
internal/e2e/certs/server.key
Normal file
28
internal/e2e/certs/server.key
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDL+tordgqgzt06
|
||||
tCpXXUAM5WeGHiPbXhE7o3nBa3LskAhv5ivjrhmI4FPCKfIIwWjjs5urn5FEJmYd
|
||||
LyCStwsy/jKIwtTztkYrb3DmqUOFRUvvnDbqoIacKjEJLjNyJw6kj7OH+yIMwRWH
|
||||
8MbxIDOF0RmSAXIWwKKAjAIa10v/9EWIjKn9ZgeayqbJNL7TcwE6bMdP6MNLkiWC
|
||||
3/rnVVosCsiOcwQZBWFAQbsgx92rfI5JlkGwm0HgQQIDlO48iAFsJS4bx1kKeEkB
|
||||
DcL1wq30cnRSdGGkmZZas7VaZMDtYVuLN8TbxUDqCx6L5oAoUn+2L87SHyV6rTSn
|
||||
W6cnq8KzAgMBAAECggEABeYJ/hIVSkOBJpaZgi+U+mp93Q/Xu0W+m39li3WB0XEe
|
||||
1Vbjc7DlVc3K45qr6vCQ3xjNX8qm/ECyA5ZedkUAeBKIFT1bxB/EFV3NIYEejebh
|
||||
mcW7hCXCjsHc63uLCvox1F9qx9Fi0YSZ4fWmrcgKcAxfwBb2R3h9moMHTThm9GP8
|
||||
4Ur6weq+iZ4WqQtVVpifUkLKfkFNTWcOym06N8ysoP3VDXBUAJgQIFnQ6Tua8V/y
|
||||
VTpgp0jJp7MLo9TaaAVAsFALvufAzSTdb7rmZsZxN6X/4ftHkvN+IchnOZdPnqEo
|
||||
roQnRpD42eDSkWA3KLHjqFpBxMpWv5QE2tpxn/WYgQKBgQDx08RB4SWbT0qyFNDf
|
||||
sPDkpAy+jmXtqIr1Q8TY095HUXrGTJSMuyI27UwIOPEOuVgR5RLLdm981w98U723
|
||||
qgCQSLgWCDhManY117elPBGPdh9eTIhVPGGeJVfPdZJv86E9xgJMEH2T7rQX9XrD
|
||||
HZeP5M9vJzDopnWYiBtUDGjwgQKBgQDX7z8ityqLz1JC2nBK2TW8HFZXdOFtmOS3
|
||||
glnhhsVXBkQtfdDahr2JxSIPVs41AHnZ6YoTAuLP2BZNfM2/eJJ3v51x6VimycsE
|
||||
yPqDh6Slzy+Zxi/rPHdPosT3kVB/fw9Mh5EhKim+ePD6zwyAg/VbQ6TAyL8RGLwF
|
||||
L9krz6dZMwKBgQDrZFgITJFfsXojCDIdW83QsYMaReAfy0yf2zyUVv9p6n0Y41hB
|
||||
+LIgRzIYWt0CmRWKLbHyzTFOksSnH5hvXBK6X56ZC51JfOPeG0HOnD73p2J3VwXp
|
||||
+iGHYBAuh+JoCP1t9P0tb2vdgKBbJf8+R+LNFUt5heuMuuOC+xGm6YGIgQKBgQCg
|
||||
PeECaB7gpbeL+QI/6TThJb5FJG1toXqJW+hLXnHvhKzvfWCR6k+Zf3spGufnmTTT
|
||||
q2VVelzKlLboReeBYX5/8uhp8hphm3pXULXnCa/7g+b1HIfO744ywL2eYm30T1wR
|
||||
FMF0dQ+JU8KwvRmv/T0EvwvyLIbtrwdIahulUXIE1QKBgQCwS5N/CZ3Hj+885s6C
|
||||
bRPEJg7ErgTUGu5fwXU8v+bKNtZuiEc9YwthRogmsbcIq64JLy1vkHSoNz23Gvfk
|
||||
xn3u3+FM/elxkYjGEUYEKWcqx4i2C/xBgyCGn+lkUm3J9tQoZyf4X3x5ENS3t2bE
|
||||
Z666WXF0W31ecOklmvu8fXMKUQ==
|
||||
-----END PRIVATE KEY-----
|
||||
7
internal/e2e/configs/basic.yaml
Normal file
7
internal/e2e/configs/basic.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
# E2E 测试配置 - 基础 HTTP 服务器
|
||||
server:
|
||||
listen: ":8080"
|
||||
|
||||
static:
|
||||
root: "/var/www/html"
|
||||
index: "index.html"
|
||||
10
internal/e2e/configs/proxy.yaml
Normal file
10
internal/e2e/configs/proxy.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
# E2E 测试配置 - 反向代理
|
||||
server:
|
||||
listen: ":8080"
|
||||
|
||||
proxy:
|
||||
path: "/"
|
||||
targets:
|
||||
- url: "http://backend1:80"
|
||||
- url: "http://backend2:80"
|
||||
load_balance: "round_robin"
|
||||
12
internal/e2e/configs/ssl.yaml
Normal file
12
internal/e2e/configs/ssl.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# E2E 测试配置 - SSL/TLS 服务器
|
||||
server:
|
||||
listen: ":8443"
|
||||
|
||||
ssl:
|
||||
enabled: true
|
||||
cert: "/etc/lolly/certs/server.crt"
|
||||
key: "/etc/lolly/certs/server.key"
|
||||
|
||||
static:
|
||||
root: "/var/www/html"
|
||||
index: "index.html"
|
||||
132
internal/e2e/testutil/certs.go
Normal file
132
internal/e2e/testutil/certs.go
Normal file
@ -0,0 +1,132 @@
|
||||
//go:build e2e
|
||||
|
||||
// Package testutil 提供 E2E 测试的工具函数。
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateSelfSignedCert 生成自签名证书和私钥。
|
||||
//
|
||||
// 返回证书路径、私钥路径、清理函数和错误。
|
||||
// 清理函数会删除生成的文件。
|
||||
func GenerateSelfSignedCert(tmpDir string) (certPath, keyPath string, cleanup func(), err error) {
|
||||
// 生成 ECDSA 私钥
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
// 生成序列号
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||
}
|
||||
|
||||
// 创建证书模板
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Lolly E2E Test"},
|
||||
CommonName: "localhost",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: nil,
|
||||
}
|
||||
|
||||
// 生成证书
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to create certificate: %w", err)
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
|
||||
// 写入证书文件
|
||||
certPath = filepath.Join(tmpDir, "cert.pem")
|
||||
certFile, err := os.Create(certPath)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to create cert file: %w", err)
|
||||
}
|
||||
if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
certFile.Close()
|
||||
return "", "", nil, fmt.Errorf("failed to write cert: %w", err)
|
||||
}
|
||||
certFile.Close()
|
||||
|
||||
// 写入私钥文件
|
||||
keyPath = filepath.Join(tmpDir, "key.pem")
|
||||
keyFile, err := os.Create(keyPath)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to create key file: %w", err)
|
||||
}
|
||||
privBytes, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
keyFile.Close()
|
||||
return "", "", nil, fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}); err != nil {
|
||||
keyFile.Close()
|
||||
return "", "", nil, fmt.Errorf("failed to write key: %w", err)
|
||||
}
|
||||
keyFile.Close()
|
||||
|
||||
// 清理函数
|
||||
cleanup = func() {
|
||||
os.Remove(certPath)
|
||||
os.Remove(keyPath)
|
||||
}
|
||||
|
||||
return certPath, keyPath, cleanup, nil
|
||||
}
|
||||
|
||||
// GenerateCertPool 从证书文件创建 x509.CertPool。
|
||||
func GenerateCertPool(certPath string) (*x509.CertPool, error) {
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read cert file: %w", err)
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM(certPEM) {
|
||||
return nil, fmt.Errorf("failed to append cert to pool")
|
||||
}
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
// GenerateTLSConfig 生成客户端 TLS 配置,信任指定的证书。
|
||||
func GenerateTLSConfig(certPath string) (*TLSConfig, error) {
|
||||
certPool, err := GenerateCertPool(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TLSConfig{
|
||||
RootCAs: certPool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TLSConfig 简化的 TLS 配置。
|
||||
type TLSConfig struct {
|
||||
RootCAs *x509.CertPool
|
||||
}
|
||||
100
internal/e2e/testutil/concurrent.go
Normal file
100
internal/e2e/testutil/concurrent.go
Normal file
@ -0,0 +1,100 @@
|
||||
//go:build e2e
|
||||
|
||||
// Package testutil 提供 E2E 测试的工具函数。
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConcurrentRequestConfig 并发请求配置。
|
||||
type ConcurrentRequestConfig struct {
|
||||
URL string
|
||||
Count int
|
||||
Timeout time.Duration
|
||||
ExpectCode int
|
||||
}
|
||||
|
||||
// ConcurrentRequestResult 并发请求结果。
|
||||
type ConcurrentRequestResult struct {
|
||||
Index int
|
||||
StatusCode int
|
||||
Error error
|
||||
}
|
||||
|
||||
// RunConcurrentRequests 运行并发请求并返回结果。
|
||||
//
|
||||
// 使用 sync.WaitGroup 实现真正的并发。
|
||||
// 返回所有请求的结果,包括状态码和错误。
|
||||
func RunConcurrentRequests(cfg ConcurrentRequestConfig) []ConcurrentRequestResult {
|
||||
results := make([]ConcurrentRequestResult, cfg.Count)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
}
|
||||
|
||||
for i := 0; i < cfg.Count; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
result := ConcurrentRequestResult{Index: index}
|
||||
|
||||
resp, err := client.Get(cfg.URL)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("request %d failed: %w", index, err)
|
||||
results[index] = result
|
||||
return
|
||||
}
|
||||
|
||||
// 读取并丢弃响应体,确保连接可复用
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
result.StatusCode = resp.StatusCode
|
||||
results[index] = result
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// VerifyConcurrentResults 验证并发请求结果。
|
||||
//
|
||||
// 检查所有请求是否返回预期状态码。
|
||||
// 返回失败的请求列表。
|
||||
func VerifyConcurrentResults(t *testing.T, results []ConcurrentRequestResult, expectCode int) []ConcurrentRequestResult {
|
||||
var failures []ConcurrentRequestResult
|
||||
|
||||
for _, r := range results {
|
||||
if r.Error != nil {
|
||||
t.Logf("Request %d error: %v", r.Index, r.Error)
|
||||
failures = append(failures, r)
|
||||
continue
|
||||
}
|
||||
|
||||
if r.StatusCode != expectCode {
|
||||
t.Logf("Request %d: expected %d, got %d", r.Index, expectCode, r.StatusCode)
|
||||
failures = append(failures, r)
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
||||
|
||||
// RunAndVerifyConcurrentRequests 运行并发请求并验证结果。
|
||||
//
|
||||
// 组合 RunConcurrentRequests 和 VerifyConcurrentResults。
|
||||
// 如果有任何失败,返回错误列表。
|
||||
func RunAndVerifyConcurrentRequests(t *testing.T, cfg ConcurrentRequestConfig) []ConcurrentRequestResult {
|
||||
results := RunConcurrentRequests(cfg)
|
||||
failures := VerifyConcurrentResults(t, results, cfg.ExpectCode)
|
||||
return failures
|
||||
}
|
||||
255
internal/e2e/testutil/container.go
Normal file
255
internal/e2e/testutil/container.go
Normal file
@ -0,0 +1,255 @@
|
||||
//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
|
||||
}
|
||||
60
internal/e2e/testutil/container_test.go
Normal file
60
internal/e2e/testutil/container_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
//go:build e2e
|
||||
|
||||
// Package testutil 提供 E2E 测试的工具函数。
|
||||
//
|
||||
// 包含容器管理、测试配置、等待工具等。
|
||||
//
|
||||
// 作者:xfy
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestContainerSetup 测试容器基础设施。
|
||||
func TestContainerSetup(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 检查 Docker 是否可用
|
||||
if !DockerAvailable(ctx) {
|
||||
t.Skip("Docker not available, skipping E2E tests")
|
||||
}
|
||||
|
||||
t.Log("Docker is available for E2E tests")
|
||||
}
|
||||
|
||||
// TestDockerAvailable 测试 Docker 可用性检查。
|
||||
func TestDockerAvailable(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
available := DockerAvailable(ctx)
|
||||
t.Logf("Docker available: %v", available)
|
||||
|
||||
// 不强制要求 Docker 可用,只是报告状态
|
||||
}
|
||||
|
||||
// TestMockBackendContainer 测试模拟后端容器启动。
|
||||
func TestMockBackendContainer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if !DockerAvailable(ctx) {
|
||||
t.Skip("Docker not available")
|
||||
}
|
||||
|
||||
container, addr, err := MockBackendContainer(ctx, 80)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start mock backend: %v", err)
|
||||
}
|
||||
defer container.Terminate(ctx)
|
||||
|
||||
t.Logf("Mock backend started at: %s", addr)
|
||||
|
||||
if addr == "" {
|
||||
t.Error("Expected non-empty address")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user