lolly/internal/resolver/resolver_test.go
xfy f1dd8cbeb0 fix: complete Stage 1 deep review fixes
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
2026-06-17 09:48:43 +08:00

789 lines
19 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.

package resolver
import (
"context"
"fmt"
"sync"
"testing"
"time"
"rua.plus/lolly/internal/config"
)
// TestNewResolver_NilConfig 验证 nil 解析器配置不会 panic。
func TestNewResolver_NilConfig(t *testing.T) {
r := New(nil)
if r == nil {
t.Fatal("New(nil) should return non-nil resolver")
}
if _, ok := r.(*noopResolver); !ok {
t.Error("New(nil) should return *noopResolver")
}
}
// TestNewResolver 测试解析器创建。
func TestNewResolver(t *testing.T) {
// 测试启用状态
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
IPv6: false,
}
r := New(cfg)
if r == nil {
t.Fatal("New() should return non-nil resolver")
}
// 验证是 DNSResolver 类型
dnsR, ok := r.(*DNSResolver)
if !ok {
t.Fatal("New() should return *DNSResolver when enabled")
}
if !dnsR.config.Enabled {
t.Error("config.Enabled should be true")
}
// 测试禁用状态
cfgDisabed := &config.ResolverConfig{
Enabled: false,
}
rDisabled := New(cfgDisabed)
if _, ok := rDisabled.(*noopResolver); !ok {
t.Error("New() should return *noopResolver when disabled")
}
}
// TestNewResolverDefaults 测试默认值设置。
func TestNewResolverDefaults(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
// 不设置 Valid 和 Timeout
}
r := New(cfg).(*DNSResolver)
if r.config.Valid != 30*time.Second {
t.Errorf("expected default Valid=30s, got %v", r.config.Valid)
}
if r.config.Timeout != 5*time.Second {
t.Errorf("expected default Timeout=5s, got %v", r.config.Timeout)
}
}
// TestLookupHostWithIP 测试 IP 地址直接返回。
func TestLookupHostWithIP(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
}
r := New(cfg).(*DNSResolver)
// 测试 IPv4 地址直接返回
ips, err := r.LookupHost(context.Background(), "127.0.0.1")
if err != nil {
t.Fatalf("LookupHost failed: %v", err)
}
if len(ips) != 1 || ips[0] != "127.0.0.1" {
t.Errorf("expected [127.0.0.1], got %v", ips)
}
// 测试 IPv6 地址直接返回
ips, err = r.LookupHost(context.Background(), "::1")
if err != nil {
t.Fatalf("LookupHost failed: %v", err)
}
if len(ips) != 1 || ips[0] != "::1" {
t.Errorf("expected [::1], got %v", ips)
}
}
// TestCache 测试缓存功能。
func TestCache(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{}, // 空地址,使用系统 DNS
Valid: 1 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
}
r := New(cfg).(*DNSResolver)
// 模拟缓存条目
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1", "192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 测试缓存命中
ctx := context.Background()
ips, err := r.LookupHostWithCache(ctx, "test.example.com")
if err != nil {
t.Fatalf("LookupHostWithCache failed: %v", err)
}
if len(ips) != 2 {
t.Errorf("expected 2 IPs, got %d", len(ips))
}
// 验证缓存命中统计
if r.GetCacheHits() != 1 {
t.Errorf("expected 1 cache hit, got %d", r.GetCacheHits())
}
// 测试缓存过期
// 更新缓存条目为过期
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(-1 * time.Second), // 已过期
})
// 由于使用系统 DNS可能会失败但应该尝试查询
_, _ = r.LookupHostWithCache(ctx, "test.example.com")
// 应该有缓存未命中(因为过期了)
if r.GetCacheMisses() != 1 {
t.Errorf("expected 1 cache miss, got %d", r.GetCacheMisses())
}
}
// TestIsCached 测试缓存状态检查。
func TestIsCached(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加未过期的缓存
r.storeCache("active.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 添加已过期的缓存
r.storeCache("expired.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(-1 * time.Second),
})
if !r.IsCached("active.example.com") {
t.Error("IsCached should return true for active entry")
}
if r.IsCached("expired.example.com") {
t.Error("IsCached should return false for expired entry")
}
if r.IsCached("unknown.example.com") {
t.Error("IsCached should return false for unknown entry")
}
}
// TestCacheHitRate 测试缓存命中率计算。
func TestCacheHitRate(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 初始命中率应为 0
if r.GetHitRate() != 0 {
t.Errorf("expected 0 hit rate, got %f", r.GetHitRate())
}
// 模拟缓存命中
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 3 次命中
for range 3 {
_, _ = r.LookupHostWithCache(context.Background(), "test.example.com")
}
// 1 次未命中(新域名)
_, _ = r.LookupHostWithCache(context.Background(), "unknown.example.com")
// 命中率应为 3/4 = 0.75
hitRate := r.GetHitRate()
if hitRate != 0.75 {
t.Errorf("expected 0.75 hit rate, got %f", hitRate)
}
}
// TestStats 测试统计信息。
func TestStats(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加缓存条目
r.storeCache("test1.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("test2.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test1.example.com"] = struct{}{}
r.refreshHosts["test2.example.com"] = struct{}{}
r.mu.Unlock()
// 触发缓存命中
_, _ = r.LookupHostWithCache(context.Background(), "test1.example.com")
stats := r.Stats()
if stats.CacheHits != 1 {
t.Errorf("expected 1 cache hit, got %d", stats.CacheHits)
}
if stats.CacheEntries != 2 {
t.Errorf("expected 2 cache entries, got %d", stats.CacheEntries)
}
}
// TestResetStats 测试统计重置。
func TestResetStats(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加统计数据
r.hits.Store(10)
r.misses.Store(5)
r.errors.Store(2)
r.latencyNs.Store(1000000)
r.count.Store(10)
r.ResetStats()
if r.GetCacheHits() != 0 {
t.Errorf("expected 0 hits after reset, got %d", r.GetCacheHits())
}
if r.GetCacheMisses() != 0 {
t.Errorf("expected 0 misses after reset, got %d", r.GetCacheMisses())
}
if r.GetResolveErrors() != 0 {
t.Errorf("expected 0 errors after reset, got %d", r.GetResolveErrors())
}
if r.GetAverageLatency() != 0 {
t.Errorf("expected 0 latency after reset, got %v", r.GetAverageLatency())
}
}
// TestStartStop 测试启动和停止。
func TestStartStop(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 启动
err := r.Start()
if err != nil {
t.Fatalf("Start failed: %v", err)
}
if !r.started.Load() {
t.Error("resolver should be started")
}
// 重复启动不应报错
err = r.Start()
if err != nil {
t.Errorf("Start() should not error when already started: %v", err)
}
// 停止
err = r.Stop()
if err != nil {
t.Fatalf("Stop failed: %v", err)
}
if r.started.Load() {
t.Error("resolver should be stopped")
}
}
// TestStartStopStart 验证 resolver 在 Stop 后可以重新 Start。
func TestStartStopStart(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 第一次启动
if err := r.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}
if !r.started.Load() {
t.Error("resolver should be started")
}
// 停止
if err := r.Stop(); err != nil {
t.Fatalf("Stop failed: %v", err)
}
if r.started.Load() {
t.Error("resolver should be stopped")
}
// 再次启动:验证 stopCh 被正确重建refreshLoop 不会立即退出
if err := r.Start(); err != nil {
t.Fatalf("second Start failed: %v", err)
}
if !r.started.Load() {
t.Error("resolver should be started again")
}
// 等待一段时间,确认 refreshLoop 仍在运行
time.Sleep(50 * time.Millisecond)
if !r.started.Load() {
t.Error("resolver should still be running after sleep")
}
_ = r.Stop()
}
// TestDeleteCacheEntry 测试删除缓存条目。
func TestDeleteCacheEntry(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加缓存
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 删除
r.DeleteCacheEntry("test.example.com")
// 验证删除
if r.IsCached("test.example.com") {
t.Error("cache entry should be deleted")
}
}
// TestClearCache 测试清空缓存。
func TestClearCache(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加多个缓存
for i := range 5 {
host := fmt.Sprintf("test%d.example.com", i)
r.storeCache(host, &DNSCacheEntry{
IPs: []string{fmt.Sprintf("192.168.1.%d", i)},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts[host] = struct{}{}
r.mu.Unlock()
}
// 清空
r.ClearCache()
// 验证
stats := r.GetCacheStats()
if stats.Entries != 0 {
t.Errorf("expected 0 entries after clear, got %d", stats.Entries)
}
}
// TestConcurrentAccess 测试并发访问。
func TestConcurrentAccess(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 添加测试缓存
r.storeCache("test.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.mu.Lock()
r.refreshHosts["test.example.com"] = struct{}{}
r.mu.Unlock()
// 并发读取
var wg sync.WaitGroup
for range 100 {
wg.Go(func() {
_, _ = r.LookupHostWithCache(context.Background(), "test.example.com")
})
}
wg.Wait()
// 验证没有竞争条件导致的 panic
if r.GetCacheHits() != 100 {
t.Errorf("expected 100 cache hits, got %d", r.GetCacheHits())
}
}
// TestNoopResolver 测试空解析器。
func TestNoopResolver(t *testing.T) {
nr := &noopResolver{}
ctx := context.Background()
_, err := nr.LookupHost(ctx, "example.com")
if err == nil {
t.Error("noopResolver.LookupHost should return error")
}
_, err = nr.LookupHostWithCache(ctx, "example.com")
if err == nil {
t.Error("noopResolver.LookupHostWithCache should return error")
}
if err := nr.Refresh("example.com"); err != nil {
t.Error("noopResolver.Refresh should not return error")
}
if err := nr.Start(); err != nil {
t.Error("noopResolver.Start should not return error")
}
if err := nr.Stop(); err != nil {
t.Error("noopResolver.Stop should not return error")
}
stats := nr.Stats()
if stats.CacheHits != 0 || stats.CacheMisses != 0 {
t.Error("noopResolver.Stats should return empty stats")
}
}
// TestResolverConfigValidate 测试配置验证。
func TestResolverConfigValidate(t *testing.T) {
// 禁用状态不验证
cfg := &config.ResolverConfig{Enabled: false}
if err := cfg.Validate(); err != nil {
t.Errorf("disabled resolver should pass validation: %v", err)
}
// 启用但没有地址
cfg = &config.ResolverConfig{
Enabled: true,
}
if err := cfg.Validate(); err == nil {
t.Error("enabled resolver without addresses should fail")
}
// 有效配置
cfg = &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 30 * time.Second,
Timeout: 5 * time.Second,
IPv4: true,
IPv6: false,
}
if err := cfg.Validate(); err != nil {
t.Errorf("valid config should pass: %v", err)
}
// TTL 太短
cfg = &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
Valid: 500 * time.Millisecond,
}
if err := cfg.Validate(); err == nil {
t.Error("valid < 1s should fail")
}
// IPv4 和 IPv6 都禁用
cfg = &config.ResolverConfig{
Enabled: true,
Addresses: []string{"8.8.8.8:53"},
IPv4: false,
IPv6: false,
}
if err := cfg.Validate(); err == nil {
t.Error("both IPv4 and IPv6 disabled should fail")
}
}
// TestResolverConfigTTL 测试 TTL 方法。
func TestResolverConfigTTL(t *testing.T) {
cfg := &config.ResolverConfig{
Valid: 60 * time.Second,
}
if cfg.TTL() != 60*time.Second {
t.Errorf("expected TTL=60s, got %v", cfg.TTL())
}
}
// TestRefresh 测试刷新方法。
func TestRefresh(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
}
r := New(cfg).(*DNSResolver)
// 测试 IP 地址直接返回(无 DNS 查询)
err := r.Refresh("127.0.0.1")
if err != nil {
t.Errorf("Refresh for IP should succeed: %v", err)
}
}
// TestCacheStats 测试缓存统计。
func TestCacheStats(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 1 * time.Second, // 短 TTL 用于测试过期
}
r := New(cfg).(*DNSResolver)
// 添加活跃缓存
r.storeCache("active.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 添加过期缓存
r.storeCache("expired.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(-1 * time.Second),
})
r.mu.Lock()
r.refreshHosts["active.example.com"] = struct{}{}
r.refreshHosts["expired.example.com"] = struct{}{}
r.mu.Unlock()
// 设置命中/未命中统计
r.hits.Store(10)
r.misses.Store(5)
stats := r.GetCacheStats()
if stats.Hits != 10 {
t.Errorf("expected 10 hits, got %d", stats.Hits)
}
if stats.Misses != 5 {
t.Errorf("expected 5 misses, got %d", stats.Misses)
}
if stats.Entries != 2 {
t.Errorf("expected 2 entries, got %d", stats.Entries)
}
if stats.Expired != 1 {
t.Errorf("expected 1 expired, got %d", stats.Expired)
}
}
// TestCacheSizeLimit 测试缓存大小限制。
func TestCacheSizeLimit(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3, // 限制 3 个条目
}
r := New(cfg).(*DNSResolver)
// 添加 5 个缓存条目,应淘汰 2 个
for i := range 5 {
host := fmt.Sprintf("host%d.example.com", i)
r.storeCache(host, &DNSCacheEntry{
IPs: []string{fmt.Sprintf("192.168.1.%d", i)},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
}
// 验证缓存条目数不超过限制
stats := r.GetCacheStats()
if stats.Entries > 3 {
t.Errorf("expected at most 3 entries with CacheSize=3, got %d", stats.Entries)
}
// 验证最早添加的条目被淘汰LRU
if r.IsCached("host0.example.com") {
t.Error("host0.example.com should be evicted (oldest)")
}
if r.IsCached("host1.example.com") {
t.Error("host1.example.com should be evicted (second oldest)")
}
// 验证最新添加的条目存在
if !r.IsCached("host2.example.com") {
t.Error("host2.example.com should be cached")
}
if !r.IsCached("host3.example.com") {
t.Error("host3.example.com should be cached")
}
if !r.IsCached("host4.example.com") {
t.Error("host4.example.com should be cached")
}
}
// TestCacheSizeZero 测试 cache_size=0 时无限制。
func TestCacheSizeZero(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 0, // 无限制
}
r := New(cfg).(*DNSResolver)
// 添加大量缓存条目
for i := range 100 {
host := fmt.Sprintf("host%d.example.com", i)
r.storeCache(host, &DNSCacheEntry{
IPs: []string{fmt.Sprintf("192.168.1.%d", i%256)},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
}
// 验证所有条目都存在
stats := r.GetCacheStats()
if stats.Entries != 100 {
t.Errorf("expected 100 entries with CacheSize=0, got %d", stats.Entries)
}
}
// TestLRUEvictionOrder 测试 LRU 淘汰顺序。
func TestLRUEvictionOrder(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3,
}
r := New(cfg).(*DNSResolver)
// 添加 3 个条目填满缓存
r.storeCache("a.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("b.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("c.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.3"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 访问 a.example.com 使其变为最新
r.storeCache("a.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 添加新条目,应淘汰 b.example.com最久未使用
r.storeCache("d.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.4"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 验证淘汰顺序
if r.IsCached("b.example.com") {
t.Error("b.example.com should be evicted (least recently used)")
}
if !r.IsCached("a.example.com") {
t.Error("a.example.com should be cached (recently accessed)")
}
if !r.IsCached("c.example.com") {
t.Error("c.example.com should be cached")
}
if !r.IsCached("d.example.com") {
t.Error("d.example.com should be cached (newly added)")
}
}
// TestCacheUpdatePreservesOrder 测试更新已存在条目不触发淘汰。
func TestCacheUpdatePreservesOrder(t *testing.T) {
cfg := &config.ResolverConfig{
Enabled: true,
Valid: 30 * time.Second,
CacheSize: 3,
}
r := New(cfg).(*DNSResolver)
// 添加 3 个条目填满缓存
r.storeCache("a.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.1"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("b.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.2"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
r.storeCache("c.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.3"},
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 更新已存在的条目(不应触发淘汰)
r.storeCache("b.example.com", &DNSCacheEntry{
IPs: []string{"192.168.1.20"}, // 新 IP
ExpiresAt: time.Now().Add(1 * time.Minute),
})
// 验证所有条目仍然存在
stats := r.GetCacheStats()
if stats.Entries != 3 {
t.Errorf("expected 3 entries after update, got %d", stats.Entries)
}
// 验证更新生效
entry, ok := r.GetCacheEntry("b.example.com")
if !ok {
t.Fatal("b.example.com should exist")
}
if len(entry.IPs) != 1 || entry.IPs[0] != "192.168.1.20" {
t.Errorf("expected IP 192.168.1.20, got %v", entry.IPs)
}
}