Batch 1 - Crash/data corruption: - Add nil/empty config guards in server and app - Protect against fasthttp buffer mutation in logging - Ensure mimeutil falls back before caching empty MIME - Make proxy health checker and resolver tolerate nil cfg - Prevent Lua EMERG/ALERT/CRIT log levels from killing process Batch 2 - Concurrency/race conditions: - Add focused race tests for compression pool, ssl/ocsp, server proxies slice, Lua timer cancel, and TCP socket connect - Fix Lua timer active race and socket ConnectAsync race Batch 3 - Resource leaks/functional damage: - Track and stop rate limiter cleanup goroutines on shutdown - Add tests for proxy connection count leaks, server pool deadlock, Linux sendfile integrity, WebSocket frame data loss, stream UDP stop deadlock, and upstream name mismatch - Fix stream ListenTCP/upstream lookup and UDP shutdown Batch 4 - High severity technical debt: - Add nil guard in security headers middleware - Validate sliding window divisor and add tests - Protect static handler fields with existing RWMutex - Avoid mutating live HostClient.Addr on DNS updates - Fix slow start Start/Stop and resolver restart - Initialize variable fallback context maps - Add tests for request_id/time_local and logging file handle close Also: - Relax integration variable performance threshold under race detector - Update benchmark plan docs for new stream ListenTCP signature Verification: - make test: pass - go test -race -count=1 ./internal/...: pass - make lint: 0 issues - make build: success
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
// Package proxy 提供 DNS 代理功能的测试。
|
||
//
|
||
// 该文件测试 proxy_dns.go 中的 DNS 相关功能,包括:
|
||
// - DNS 解析器设置和获取
|
||
// - DNS 缓存刷新机制
|
||
// - HostClient 地址更新
|
||
// - 错误处理场景
|
||
//
|
||
// 作者:xfy
|
||
package proxy
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/loadbalance"
|
||
"rua.plus/lolly/internal/resolver"
|
||
)
|
||
|
||
// mockResolver 实现 resolver.Resolver 接口的模拟解析器。
|
||
type mockResolver struct {
|
||
mu sync.RWMutex
|
||
lookupResults map[string][]string
|
||
lookupError error
|
||
startErr error
|
||
stopErr error
|
||
lookupHostCalls int
|
||
lookupWithCacheCalls int
|
||
startCalls int
|
||
stopCalls int
|
||
}
|
||
|
||
func (m *mockResolver) LookupHost(_ context.Context, host string) ([]string, error) {
|
||
m.mu.Lock()
|
||
m.lookupHostCalls++
|
||
m.mu.Unlock()
|
||
return m.resolve(host)
|
||
}
|
||
|
||
func (m *mockResolver) LookupHostWithCache(_ context.Context, host string) ([]string, error) {
|
||
m.mu.Lock()
|
||
m.lookupWithCacheCalls++
|
||
m.mu.Unlock()
|
||
return m.resolve(host)
|
||
}
|
||
|
||
func (m *mockResolver) resolve(host string) ([]string, error) {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
|
||
if m.lookupError != nil {
|
||
return nil, m.lookupError
|
||
}
|
||
if ips, ok := m.lookupResults[host]; ok {
|
||
return ips, nil
|
||
}
|
||
return nil, errors.New("host not found in mock resolver")
|
||
}
|
||
|
||
func (m *mockResolver) Refresh(_ string) error {
|
||
return nil
|
||
}
|
||
|
||
func (m *mockResolver) Start() error {
|
||
m.mu.Lock()
|
||
m.startCalls++
|
||
m.mu.Unlock()
|
||
return m.startErr
|
||
}
|
||
|
||
func (m *mockResolver) Stop() error {
|
||
m.mu.Lock()
|
||
m.stopCalls++
|
||
m.mu.Unlock()
|
||
return m.stopErr
|
||
}
|
||
|
||
func (m *mockResolver) Stats() resolver.Stats {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
return resolver.Stats{
|
||
CacheHits: int64(m.lookupWithCacheCalls),
|
||
CacheMisses: int64(m.lookupHostCalls),
|
||
}
|
||
}
|
||
|
||
// TestSetResolver 测试设置 DNS 解析器。
|
||
func TestSetResolver(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://localhost:8080"},
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
// 初始状态 resolver 为 nil
|
||
if p.resolver != nil {
|
||
t.Error("resolver should be nil initially")
|
||
}
|
||
|
||
// 设置解析器
|
||
mr := &mockResolver{}
|
||
p.SetResolver(mr)
|
||
|
||
if p.resolver != mr {
|
||
t.Error("SetResolver() did not set resolver correctly")
|
||
}
|
||
}
|
||
|
||
// TestGetResolverStats_NoResolver 测试没有解析器时返回空统计。
|
||
|
||
// TestGetResolverStats_WithResolver 测试有解析器时返回统计。
|
||
|
||
// TestStartWithResolver 测试启动代理时解析器正确启动。
|
||
|
||
// TestStartResolverFails 测试解析器启动失败时代理返回错误。
|
||
|
||
// TestStartIdempotent 测试 Start 是幂等的。
|
||
|
||
// TestStopIdempotent 测试 Stop 是幂等的。
|
||
|
||
// TestStopWithoutResolver 测试没有解析器时停止代理。
|
||
|
||
// TestRefreshDNS_Success 测试 DNS 刷新成功场景。
|
||
|
||
// TestRefreshDNS_LookupError 测试 DNS 刷新时查找失败场景。
|
||
|
||
// TestRefreshDNS_NoResolver 测试没有解析器时刷新不执行任何操作。
|
||
|
||
// TestRefreshDNS_IPAddressTarget 测试 IP 类型的目标不需要解析。
|
||
|
||
// TestRefreshDNS_RecentlyResolved 测试最近已解析的目标不需要再次解析。
|
||
|
||
// TestRefreshDNS_ExpiredResolve 测试 TTL 过期后需要重新解析。
|
||
// 该测试验证 TTL 过期检查的正确性:
|
||
// - 短时间内(< TTL)不需要重新解析
|
||
// - 长时间后(> TTL)需要重新解析
|
||
|
||
// TestUpdateHostClientAddr_HTTP 测试 HTTP 目标地址更新。
|
||
|
||
// TestUpdateHostClientAddr_HTTPS 测试 HTTPS 目标地址更新。
|
||
|
||
// TestUpdateHostClientAddr_DefaultPort 测试没有端口时使用默认端口。
|
||
|
||
// TestUpdateHostClientAddr_NonExistentTarget 测试不存在的目标不更新。
|
||
|
||
// TestGetResolverTTL 测试 TTL 获取。
|
||
|
||
// TestDNSRefreshLoop_StartStop 测试 DNS 刷新循环的启动和停止。
|
||
|
||
// TestMultipleTargets_Refresh 测试多目标刷新。
|
||
|
||
// TestUpdateHostClientAddr_ReplacesClient 验证 DNS 更新时重建 HostClient 而不是修改 Addr。
|
||
func TestUpdateHostClientAddr_ReplacesClient(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend.example.com:8080"},
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
oldClient := p.clients["http://backend.example.com:8080"]
|
||
if oldClient == nil {
|
||
t.Fatal("old client should not be nil")
|
||
}
|
||
oldAddr := oldClient.Addr
|
||
|
||
p.updateHostClientAddr(targets[0], "192.168.1.100")
|
||
|
||
newClient := p.clients["http://backend.example.com:8080"]
|
||
if newClient == nil {
|
||
t.Fatal("new client should not be nil")
|
||
}
|
||
if newClient == oldClient {
|
||
t.Error("updateHostClientAddr should replace the HostClient instead of mutating it")
|
||
}
|
||
if newClient.Addr != "192.168.1.100:8080" {
|
||
t.Errorf("new client addr = %q, want %q", newClient.Addr, "192.168.1.100:8080")
|
||
}
|
||
// 旧 client 的 Addr 不应被修改,旧连接继续使用
|
||
if oldClient.Addr != oldAddr {
|
||
t.Errorf("old client addr was mutated: got %q, want %q", oldClient.Addr, oldAddr)
|
||
}
|
||
}
|
||
|
||
// TestUpdateHostClientAddr_WithProxyBind 验证配置了 proxy_bind 时使用正确的 client key。
|
||
func TestUpdateHostClientAddr_WithProxyBind(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
ProxyBind: "127.0.0.1",
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend.example.com:8080"},
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
key := "http://backend.example.com:8080|127.0.0.1"
|
||
if p.clients[key] == nil {
|
||
t.Fatalf("client with proxy_bind key should exist")
|
||
}
|
||
|
||
p.updateHostClientAddr(targets[0], "192.168.1.200")
|
||
|
||
newClient := p.clients[key]
|
||
if newClient == nil {
|
||
t.Fatal("new client should not be nil")
|
||
}
|
||
if newClient.Addr != "192.168.1.200:8080" {
|
||
t.Errorf("new client addr = %q, want %q", newClient.Addr, "192.168.1.200:8080")
|
||
}
|
||
}
|
||
|
||
// TestUpdateHostClientAddr_ConcurrentRead 验证更新 Addr 时不会与 getClient 产生数据竞争。
|
||
func TestUpdateHostClientAddr_ConcurrentRead(t *testing.T) {
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{Connect: 5 * time.Second},
|
||
}
|
||
targets := []*loadbalance.Target{
|
||
{URL: "http://backend.example.com:8080"},
|
||
}
|
||
|
||
p, err := NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
// 并发读取 client
|
||
for range 10 {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
client := p.getClient(targets[0].URL)
|
||
_ = client.Addr
|
||
}()
|
||
}
|
||
|
||
// 并发更新 client
|
||
for i := range 10 {
|
||
wg.Add(1)
|
||
go func(id int) {
|
||
defer wg.Done()
|
||
p.updateHostClientAddr(targets[0], "192.168.1."+string(rune('0'+id)))
|
||
}(i)
|
||
}
|
||
|
||
wg.Wait()
|
||
}
|
||
|
||
// TestStopResolverFails 测试停止解析器失败时返回错误。
|