添加配置生成、常量定义、测试设置、SSL 和 WebSocket 工具函数。 重构 container.go 支持函数式选项模式配置容器。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
585 lines
13 KiB
Go
585 lines
13 KiB
Go
//go:build e2e
|
||
|
||
// Package testutil 提供 E2E 测试的工具函数。
|
||
//
|
||
// 包含动态配置生成器,支持编程方式生成 YAML 配置文件。
|
||
//
|
||
// 作者:xfy
|
||
package testutil
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
)
|
||
|
||
// ConfigBuilder 动态配置构建器。
|
||
//
|
||
// 支持编程方式生成 YAML 配置,用于 E2E 测试场景。
|
||
// 提供链式调用方式,方便组合不同配置。
|
||
//
|
||
// 使用示例:
|
||
//
|
||
// cfg := testutil.NewConfigBuilder().
|
||
// WithServer(":8080").
|
||
// WithProxy("/api/", targets).
|
||
// WithSSL(certPath, keyPath).
|
||
// Build()
|
||
type ConfigBuilder struct {
|
||
cfg *config.Config
|
||
}
|
||
|
||
// NewConfigBuilder 创建配置构建器。
|
||
func NewConfigBuilder() *ConfigBuilder {
|
||
return &ConfigBuilder{
|
||
cfg: &config.Config{
|
||
Servers: []config.ServerConfig{},
|
||
},
|
||
}
|
||
}
|
||
|
||
// WithServer 添加服务器配置。
|
||
//
|
||
// 参数:
|
||
// - listen: 监听地址,如 ":8080"
|
||
//
|
||
// 返回构建器以支持链式调用。
|
||
func (b *ConfigBuilder) WithServer(listen string) *ConfigBuilder {
|
||
b.cfg.Servers = append(b.cfg.Servers, config.ServerConfig{
|
||
Listen: listen,
|
||
})
|
||
return b
|
||
}
|
||
|
||
// WithServerConfig 添加完整服务器配置。
|
||
func (b *ConfigBuilder) WithServerConfig(server config.ServerConfig) *ConfigBuilder {
|
||
b.cfg.Servers = append(b.cfg.Servers, server)
|
||
return b
|
||
}
|
||
|
||
// ProxyTargetOption 代理目标选项。
|
||
type ProxyTargetOption func(*config.ProxyTarget)
|
||
|
||
// WithWeight 设置权重。
|
||
func WithWeight(weight int) ProxyTargetOption {
|
||
return func(t *config.ProxyTarget) {
|
||
t.Weight = weight
|
||
}
|
||
}
|
||
|
||
// WithMaxConns 设置最大连接数。
|
||
func WithMaxConns(maxConns int) ProxyTargetOption {
|
||
return func(t *config.ProxyTarget) {
|
||
t.MaxConns = maxConns
|
||
}
|
||
}
|
||
|
||
// WithMaxFails 设置最大失败次数。
|
||
func WithMaxFails(maxFails int, failTimeout time.Duration) ProxyTargetOption {
|
||
return func(t *config.ProxyTarget) {
|
||
t.MaxFails = maxFails
|
||
t.FailTimeout = failTimeout
|
||
}
|
||
}
|
||
|
||
// WithBackup 设置为备份服务器。
|
||
func WithBackup() ProxyTargetOption {
|
||
return func(t *config.ProxyTarget) {
|
||
t.Backup = true
|
||
}
|
||
}
|
||
|
||
// ProxyOption 代理配置选项。
|
||
type ProxyOption func(*config.ProxyConfig)
|
||
|
||
// ProxyConfig 代理配置类型别名。
|
||
type ProxyConfig = config.ProxyConfig
|
||
|
||
// WithLoadBalance 设置负载均衡算法。
|
||
func WithLoadBalance(algorithm string) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.LoadBalance = algorithm
|
||
}
|
||
}
|
||
|
||
// WithHealthCheck 设置健康检查。
|
||
func WithHealthCheck(path string, interval, timeout time.Duration) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.HealthCheck = config.HealthCheckConfig{
|
||
Path: path,
|
||
Interval: interval,
|
||
Timeout: timeout,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxyTimeout 设置代理超时。
|
||
func WithProxyTimeout(connect, read, write time.Duration) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.Timeout = config.ProxyTimeout{
|
||
Connect: connect,
|
||
Read: read,
|
||
Write: write,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxyHeaders 设置代理头部。
|
||
func WithProxyHeaders(setRequest, setResponse map[string]string) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.Headers = config.ProxyHeaders{
|
||
SetRequest: setRequest,
|
||
SetResponse: setResponse,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxyCache 设置代理缓存。
|
||
func WithProxyCache(maxAge time.Duration, cacheLock bool) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.Cache = config.ProxyCacheConfig{
|
||
Enabled: true,
|
||
MaxAge: maxAge,
|
||
CacheLock: cacheLock,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxySSL 设置上游 SSL。
|
||
func WithProxySSL(serverName string, insecureSkipVerify bool) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.ProxySSL = &config.ProxySSLConfig{
|
||
Enabled: true,
|
||
ServerName: serverName,
|
||
InsecureSkipVerify: insecureSkipVerify,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxyBuffering 设置代理缓冲。
|
||
func WithProxyBuffering(mode string, bufferSize int) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.Buffering = &config.ProxyBufferingConfig{
|
||
Mode: mode,
|
||
BufferSize: bufferSize,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxyNextUpstream 设置故障转移。
|
||
func WithProxyNextUpstream(tries int, httpCodes []int) ProxyOption {
|
||
return func(p *config.ProxyConfig) {
|
||
p.NextUpstream = config.NextUpstreamConfig{
|
||
Tries: tries,
|
||
HTTPCodes: httpCodes,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithProxy 添加代理配置。
|
||
//
|
||
// 参数:
|
||
// - path: 代理路径前缀
|
||
// - urls: 后端 URL 列表
|
||
// - opts: 可选配置选项
|
||
//
|
||
// 返回构建器以支持链式调用。
|
||
func (b *ConfigBuilder) WithProxy(path string, urls []string, opts ...ProxyOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8080")
|
||
}
|
||
|
||
targets := make([]config.ProxyTarget, len(urls))
|
||
for i, url := range urls {
|
||
targets[i] = config.ProxyTarget{
|
||
URL: url,
|
||
}
|
||
}
|
||
|
||
proxy := config.ProxyConfig{
|
||
Path: path,
|
||
Targets: targets,
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&proxy)
|
||
}
|
||
|
||
// 添加到第一个服务器
|
||
b.cfg.Servers[0].Proxy = append(b.cfg.Servers[0].Proxy, proxy)
|
||
return b
|
||
}
|
||
|
||
// WithProxyTargets 添加带选项的代理配置。
|
||
//
|
||
// targetOptsPerTarget 是每个目标的选项列表(索引对应 urls)。
|
||
// opts 是代理级别的选项。
|
||
func (b *ConfigBuilder) WithProxyTargets(path string, urls []string, targetOptsPerTarget [][]ProxyTargetOption, opts ...ProxyOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8080")
|
||
}
|
||
|
||
targets := make([]config.ProxyTarget, len(urls))
|
||
for i, url := range urls {
|
||
targets[i] = config.ProxyTarget{
|
||
URL: url,
|
||
}
|
||
// 应用该目标的选项
|
||
if i < len(targetOptsPerTarget) {
|
||
for _, opt := range targetOptsPerTarget[i] {
|
||
opt(&targets[i])
|
||
}
|
||
}
|
||
}
|
||
|
||
proxy := config.ProxyConfig{
|
||
Path: path,
|
||
Targets: targets,
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&proxy)
|
||
}
|
||
|
||
b.cfg.Servers[0].Proxy = append(b.cfg.Servers[0].Proxy, proxy)
|
||
return b
|
||
}
|
||
|
||
// SSLOption SSL 配置选项。
|
||
type SSLOption func(*config.SSLConfig)
|
||
|
||
// WithHTTP2 启用 HTTP/2。
|
||
func WithHTTP2(enabled bool, maxConcurrentStreams int) SSLOption {
|
||
return func(s *config.SSLConfig) {
|
||
s.HTTP2 = config.HTTP2Config{
|
||
Enabled: enabled,
|
||
MaxConcurrentStreams: maxConcurrentStreams,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithTLSProtocols 设置 TLS 协议版本。
|
||
func WithTLSProtocols(protocols []string) SSLOption {
|
||
return func(s *config.SSLConfig) {
|
||
s.Protocols = protocols
|
||
}
|
||
}
|
||
|
||
// WithSessionTickets 启用 Session Tickets。
|
||
func WithSessionTickets(enabled bool) SSLOption {
|
||
return func(s *config.SSLConfig) {
|
||
s.SessionTickets = config.SessionTicketsConfig{
|
||
Enabled: enabled,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithHSTS 配置 HSTS。
|
||
func WithHSTS(maxAge int, includeSubDomains bool) SSLOption {
|
||
return func(s *config.SSLConfig) {
|
||
s.HSTS = config.HSTSConfig{
|
||
MaxAge: maxAge,
|
||
IncludeSubDomains: includeSubDomains,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithSSL 配置 SSL/TLS。
|
||
//
|
||
// 参数:
|
||
// - cert: 证书文件路径
|
||
// - key: 私钥文件路径
|
||
// - opts: 可选 SSL 配置
|
||
//
|
||
// 返回构建器以支持链式调用。
|
||
func (b *ConfigBuilder) WithSSL(cert, key string, opts ...SSLOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8443")
|
||
}
|
||
|
||
ssl := config.SSLConfig{
|
||
Cert: cert,
|
||
Key: key,
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&ssl)
|
||
}
|
||
|
||
b.cfg.Servers[0].SSL = ssl
|
||
return b
|
||
}
|
||
|
||
// StaticOption 静态文件配置选项。
|
||
type StaticOption func(*config.StaticConfig)
|
||
|
||
// WithIndex 设置索引文件。
|
||
func WithIndex(index []string) StaticOption {
|
||
return func(s *config.StaticConfig) {
|
||
s.Index = index
|
||
}
|
||
}
|
||
|
||
// WithTryFiles 设置 try_files。
|
||
func WithTryFiles(tryFiles []string) StaticOption {
|
||
return func(s *config.StaticConfig) {
|
||
s.TryFiles = tryFiles
|
||
}
|
||
}
|
||
|
||
// WithStatic 添加静态文件配置。
|
||
func (b *ConfigBuilder) WithStatic(path, root string, opts ...StaticOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8080")
|
||
}
|
||
|
||
static := config.StaticConfig{
|
||
Path: path,
|
||
Root: root,
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&static)
|
||
}
|
||
|
||
b.cfg.Servers[0].Static = append(b.cfg.Servers[0].Static, static)
|
||
return b
|
||
}
|
||
|
||
// SecurityOption 安全配置选项。
|
||
type SecurityOption func(*config.SecurityConfig)
|
||
|
||
// WithRateLimit 设置速率限制。
|
||
func WithRateLimit(requestRate, burst int) SecurityOption {
|
||
return func(s *config.SecurityConfig) {
|
||
s.RateLimit = config.RateLimitConfig{
|
||
RequestRate: requestRate,
|
||
Burst: burst,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithAccessControl 设置访问控制。
|
||
func WithAccessControl(allow, deny []string, defaultAction string) SecurityOption {
|
||
return func(s *config.SecurityConfig) {
|
||
s.Access = config.AccessConfig{
|
||
Allow: allow,
|
||
Deny: deny,
|
||
Default: defaultAction,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithBasicAuth 设置 Basic 认证。
|
||
func WithBasicAuth(users []config.User) SecurityOption {
|
||
return func(s *config.SecurityConfig) {
|
||
s.Auth = config.AuthConfig{
|
||
Type: "basic",
|
||
Users: users,
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithSecurity 配置安全选项。
|
||
func (b *ConfigBuilder) WithSecurity(opts ...SecurityOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8080")
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&b.cfg.Servers[0].Security)
|
||
}
|
||
return b
|
||
}
|
||
|
||
// CompressionOption 压缩配置选项。
|
||
type CompressionOption func(*config.CompressionConfig)
|
||
|
||
// WithCompressionType 设置压缩类型。
|
||
func WithCompressionType(typ string) CompressionOption {
|
||
return func(c *config.CompressionConfig) {
|
||
c.Type = typ
|
||
}
|
||
}
|
||
|
||
// WithCompressionLevel 设置压缩级别。
|
||
func WithCompressionLevel(level int) CompressionOption {
|
||
return func(c *config.CompressionConfig) {
|
||
c.Level = level
|
||
}
|
||
}
|
||
|
||
// WithCompressionMinSize 设置最小压缩大小。
|
||
func WithCompressionMinSize(minSize int) CompressionOption {
|
||
return func(c *config.CompressionConfig) {
|
||
c.MinSize = minSize
|
||
}
|
||
}
|
||
|
||
// WithCompression 配置压缩。
|
||
func (b *ConfigBuilder) WithCompression(opts ...CompressionOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8080")
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&b.cfg.Servers[0].Compression)
|
||
}
|
||
return b
|
||
}
|
||
|
||
// RewriteOption 重写规则选项。
|
||
type RewriteOption func(*config.RewriteRule)
|
||
|
||
// WithRewriteFlag 设置重写标志。
|
||
func WithRewriteFlag(flag string) RewriteOption {
|
||
return func(r *config.RewriteRule) {
|
||
r.Flag = flag
|
||
}
|
||
}
|
||
|
||
// WithRewrite 添加 URL 重写规则。
|
||
func (b *ConfigBuilder) WithRewrite(pattern, replacement string, opts ...RewriteOption) *ConfigBuilder {
|
||
if len(b.cfg.Servers) == 0 {
|
||
b.WithServer(":8080")
|
||
}
|
||
|
||
rule := config.RewriteRule{
|
||
Pattern: pattern,
|
||
Replacement: replacement,
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(&rule)
|
||
}
|
||
|
||
b.cfg.Servers[0].Rewrite = append(b.cfg.Servers[0].Rewrite, rule)
|
||
return b
|
||
}
|
||
|
||
// WithCachePath 配置缓存路径。
|
||
func (b *ConfigBuilder) WithCachePath(path string, maxSize int64) *ConfigBuilder {
|
||
b.cfg.CachePath = &config.ProxyCachePathConfig{
|
||
Path: path,
|
||
MaxSize: maxSize,
|
||
}
|
||
return b
|
||
}
|
||
|
||
// WithResolver 配置 DNS 解析器。
|
||
func (b *ConfigBuilder) WithResolver(addresses []string, valid, timeout time.Duration) *ConfigBuilder {
|
||
b.cfg.Resolver = config.ResolverConfig{
|
||
Enabled: true,
|
||
Addresses: addresses,
|
||
Valid: valid,
|
||
Timeout: timeout,
|
||
}
|
||
return b
|
||
}
|
||
|
||
// WithLogging 配置日志。
|
||
func (b *ConfigBuilder) WithLogging(format string) *ConfigBuilder {
|
||
b.cfg.Logging = config.LoggingConfig{
|
||
Format: format,
|
||
}
|
||
return b
|
||
}
|
||
|
||
// WithShutdown 配置关闭超时。
|
||
func (b *ConfigBuilder) WithShutdown(graceful, fast time.Duration) *ConfigBuilder {
|
||
b.cfg.Shutdown = config.ShutdownConfig{
|
||
GracefulTimeout: graceful,
|
||
FastTimeout: fast,
|
||
}
|
||
return b
|
||
}
|
||
|
||
// Build 生成 YAML 配置字符串。
|
||
//
|
||
// 返回 YAML 格式的配置字符串。
|
||
func (b *ConfigBuilder) Build() (string, error) {
|
||
data, err := yaml.Marshal(b.cfg)
|
||
if err != nil {
|
||
return "", fmt.Errorf("序列化配置失败: %w", err)
|
||
}
|
||
return string(data), nil
|
||
}
|
||
|
||
// WriteTemp 写入临时文件。
|
||
//
|
||
// 创建临时目录并写入配置文件,返回文件路径。
|
||
// 调用者负责清理临时目录。
|
||
func (b *ConfigBuilder) WriteTemp() (string, error) {
|
||
yamlStr, err := b.Build()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 创建临时目录
|
||
tmpDir, err := os.MkdirTemp("", "lolly-e2e-*")
|
||
if err != nil {
|
||
return "", fmt.Errorf("创建临时目录失败: %w", err)
|
||
}
|
||
|
||
// 写入配置文件
|
||
configPath := filepath.Join(tmpDir, "lolly.yaml")
|
||
if err := os.WriteFile(configPath, []byte(yamlStr), 0o644); err != nil {
|
||
os.RemoveAll(tmpDir)
|
||
return "", fmt.Errorf("写入配置文件失败: %w", err)
|
||
}
|
||
|
||
return configPath, nil
|
||
}
|
||
|
||
// WriteTo 写入指定目录。
|
||
func (b *ConfigBuilder) WriteTo(dir string) (string, error) {
|
||
yamlStr, err := b.Build()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 确保目录存在
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
return "", fmt.Errorf("创建目录失败: %w", err)
|
||
}
|
||
|
||
configPath := filepath.Join(dir, "lolly.yaml")
|
||
if err := os.WriteFile(configPath, []byte(yamlStr), 0o644); err != nil {
|
||
return "", fmt.Errorf("写入配置文件失败: %w", err)
|
||
}
|
||
|
||
return configPath, nil
|
||
}
|
||
|
||
// GetConfig 返回配置对象。
|
||
func (b *ConfigBuilder) GetConfig() *config.Config {
|
||
return b.cfg
|
||
}
|
||
|
||
// Reset 重置构建器。
|
||
func (b *ConfigBuilder) Reset() *ConfigBuilder {
|
||
b.cfg = &config.Config{
|
||
Servers: []config.ServerConfig{},
|
||
}
|
||
return b
|
||
}
|
||
|
||
// Clone 克隆构建器。
|
||
func (b *ConfigBuilder) Clone() *ConfigBuilder {
|
||
data, err := yaml.Marshal(b.cfg)
|
||
if err != nil {
|
||
return NewConfigBuilder()
|
||
}
|
||
|
||
var newCfg config.Config
|
||
if err := yaml.Unmarshal(data, &newCfg); err != nil {
|
||
return NewConfigBuilder()
|
||
}
|
||
|
||
return &ConfigBuilder{cfg: &newCfg}
|
||
} |