// Package resolver 提供 Mock DNS 测试。 // // 该文件测试 DNS 解析器的核心功能: // - queryDNS 成功/超时/失败 // - queryWithResolver 自定义服务器 // - refreshLoop 启动/停止 // - 缓存和 LRU 淘汰 // // 作者:xfy package resolver import ( "context" "sync" "sync/atomic" "testing" "time" "rua.plus/lolly/internal/config" ) // TestMockDNSResolverDisabled 测试禁用状态。 func TestMockDNSResolverDisabled(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: false, } resolver := New(cfg) if resolver == nil { t.Fatal("New() returned nil") } // 禁用状态下应返回错误 _, err := resolver.LookupHost(context.Background(), "example.com") if err == nil { t.Error("Expected error for disabled resolver") } } // TestMockDNSResolverEnabled 测试启用状态。 func TestMockDNSResolverEnabled(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Valid: 30 * time.Second, Timeout: 5 * time.Second, CacheSize: 100, } resolver := New(cfg) if resolver == nil { t.Fatal("New() returned nil") } // 验证类型 if _, ok := resolver.(*DNSResolver); !ok { t.Error("Expected DNSResolver type") } } // TestMockDNSResolverDefaultValues 测试默认值设置。 func TestMockDNSResolverDefaultValues(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, // 其他字段为零值 } resolver := New(cfg).(*DNSResolver) // 验证默认值 if resolver.config.Valid != 30*time.Second { t.Errorf("Expected default Valid 30s, got %v", resolver.config.Valid) } if resolver.config.Timeout != 5*time.Second { t.Errorf("Expected default Timeout 5s, got %v", resolver.config.Timeout) } if !resolver.config.IPv4 { t.Error("Expected IPv4 to be enabled by default") } } // TestMockDNSLookupHostIPAddress 测试直接 IP 地址。 func TestMockDNSLookupHostIPAddress(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, } resolver := New(cfg).(*DNSResolver) // 直接 IP 地址应直接返回 ips, err := resolver.LookupHost(context.Background(), "127.0.0.1") if err != nil { t.Fatalf("LookupHost() error: %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 = resolver.LookupHost(context.Background(), "::1") if err != nil { t.Fatalf("LookupHost() error for IPv6: %v", err) } if len(ips) != 1 || ips[0] != "::1" { t.Errorf("Expected ['::1'], got %v", ips) } } // TestMockDNSLookupHostWithCache 测试带缓存的解析。 func TestMockDNSLookupHostWithCache(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Valid: 30 * time.Second, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 第一次查询(缓存未命中) ips1, err := resolver.LookupHostWithCache(context.Background(), "localhost") if err != nil { t.Fatalf("First LookupHostWithCache() error: %v", err) } // 第二次查询(缓存命中) ips2, err := resolver.LookupHostWithCache(context.Background(), "localhost") if err != nil { t.Fatalf("Second LookupHostWithCache() error: %v", err) } // 结果应一致 if len(ips1) != len(ips2) { t.Errorf("Cache result mismatch: %v vs %v", ips1, ips2) } // 验证缓存命中 stats := resolver.Stats() if stats.CacheHits < 1 { t.Error("Expected at least one cache hit") } } // TestMockDNSCacheExpiration 测试缓存过期。 func TestMockDNSCacheExpiration(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Valid: 100 * time.Millisecond, // 短 TTL CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 第一次查询 _, err := resolver.LookupHostWithCache(context.Background(), "localhost") if err != nil { t.Fatalf("First lookup error: %v", err) } // 等待缓存过期 time.Sleep(150 * time.Millisecond) // 第二次查询应重新解析 _, err = resolver.LookupHostWithCache(context.Background(), "localhost") if err != nil { t.Fatalf("Second lookup error: %v", err) } // 验证有两次缓存未命中 stats := resolver.Stats() if stats.CacheMisses < 2 { t.Errorf("Expected at least 2 cache misses, got %d", stats.CacheMisses) } } // TestMockDNSLRUEviction 测试 LRU 淘汰。 func TestMockDNSLRUEviction(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Valid: 30 * time.Second, CacheSize: 3, // 只缓存 3 个 } resolver := New(cfg).(*DNSResolver) // 添加 3 个条目 hosts := []string{"host1.local", "host2.local", "host3.local"} for _, host := range hosts { resolver.storeCache(host, &DNSCacheEntry{ IPs: []string{"127.0.0.1"}, ExpiresAt: time.Now().Add(time.Hour), }) } // 验证缓存大小 resolver.mu.RLock() cacheLen := len(resolver.cache) resolver.mu.RUnlock() if cacheLen != 3 { t.Errorf("Expected 3 cache entries, got %d", cacheLen) } // 添加第 4 个条目,应淘汰最旧的 resolver.storeCache("host4.local", &DNSCacheEntry{ IPs: []string{"127.0.0.1"}, ExpiresAt: time.Now().Add(time.Hour), }) resolver.mu.RLock() cacheLen = len(resolver.cache) _, exists := resolver.cache["host1.local"] resolver.mu.RUnlock() if cacheLen > 3 { t.Errorf("Cache should not exceed 3 entries, got %d", cacheLen) } if exists { t.Error("Oldest entry should have been evicted") } } // TestMockDNSRefreshLoop 测试后台刷新循环。 func TestMockDNSRefreshLoop(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Valid: 200 * time.Millisecond, Timeout: 1 * time.Second, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 启动刷新循环 if err := resolver.Start(); err != nil { t.Fatalf("Start() error: %v", err) } // 验证已启动 if !resolver.started.Load() { t.Error("Resolver should be started") } // 添加一个需要刷新的主机 resolver.mu.Lock() resolver.refreshHosts["localhost"] = struct{}{} resolver.mu.Unlock() // 等待刷新 time.Sleep(300 * time.Millisecond) // 停止 if err := resolver.Stop(); err != nil { t.Fatalf("Stop() error: %v", err) } // 验证已停止 if resolver.started.Load() { t.Error("Resolver should be stopped") } } // TestMockDNSStartIdempotent 测试重复启动。 func TestMockDNSStartIdempotent(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, } resolver := New(cfg).(*DNSResolver) // 第一次启动 if err := resolver.Start(); err != nil { t.Fatalf("First Start() error: %v", err) } // 第二次启动应无操作 if err := resolver.Start(); err != nil { t.Fatalf("Second Start() error: %v", err) } // 清理 _ = resolver.Stop() } // TestMockDNSStopIdempotent 测试重复停止。 func TestMockDNSStopIdempotent(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, } resolver := New(cfg).(*DNSResolver) // 未启动时停止 if err := resolver.Stop(); err != nil { t.Fatalf("Stop() on non-started resolver error: %v", err) } // 启动 _ = resolver.Start() // 第一次停止 if err := resolver.Stop(); err != nil { t.Fatalf("First Stop() error: %v", err) } // 第二次停止应无操作 if err := resolver.Stop(); err != nil { t.Fatalf("Second Stop() error: %v", err) } } // TestMockDNSStatsFunc 测试统计信息。 func TestMockDNSStatsFunc(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 初始统计 stats := resolver.Stats() if stats.CacheHits != 0 || stats.CacheMisses != 0 { t.Error("Initial stats should be zero") } // 执行查询 _, _ = resolver.LookupHostWithCache(context.Background(), "localhost") _, _ = resolver.LookupHostWithCache(context.Background(), "localhost") stats = resolver.Stats() if stats.CacheMisses < 1 { t.Error("Expected at least one cache miss") } if stats.CacheHits < 1 { t.Error("Expected at least one cache hit") } } // TestMockDNSQueryDNSTimeout 测试 DNS 查询超时。 func TestMockDNSQueryDNSTimeout(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Timeout: 100 * time.Millisecond, Addresses: []string{"127.0.0.1:53535"}, // 不存在的 DNS 服务器 CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 使用短超时 context ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() start := time.Now() _, err := resolver.LookupHost(ctx, "example.com") elapsed := time.Since(start) // 应该超时 if err == nil { t.Error("Expected timeout error") } if elapsed > 200*time.Millisecond { t.Errorf("Query took too long: %v", elapsed) } } // TestMockDNSQueryWithResolverSystemDefault 测试使用系统默认 DNS。 func TestMockDNSQueryWithResolverSystemDefault(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, IPv4: true, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 不指定 DNS 服务器,使用系统默认 ips, err := resolver.queryWithResolver(context.Background(), "localhost", "") if err != nil { t.Fatalf("queryWithResolver() error: %v", err) } if len(ips) == 0 { t.Error("Expected at least one IP for localhost") } } // TestMockDNSConcurrentAccess 测试并发访问。 func TestMockDNSConcurrentAccess(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, CacheSize: 100, } resolver := New(cfg).(*DNSResolver) var wg sync.WaitGroup concurrency := 10 iterations := 100 for range concurrency { wg.Go(func() { for range iterations { _, _ = resolver.LookupHostWithCache(context.Background(), "localhost") } }) } wg.Wait() stats := resolver.Stats() totalOps := stats.CacheHits + stats.CacheMisses if totalOps < int64(concurrency*iterations/2) { t.Errorf("Expected more operations, got hits=%d, misses=%d", stats.CacheHits, stats.CacheMisses) } } // TestMockDNSMoveToFrontLocked 测试 LRU 移动到前端。 func TestMockDNSMoveToFrontLocked(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) resolver.cache = map[string]*DNSCacheEntry{ "a": {}, "b": {}, "c": {}, } elemA := resolver.lruList.PushFront("a") elemB := resolver.lruList.PushFront("b") elemC := resolver.lruList.PushFront("c") resolver.lruIndex["a"] = elemA resolver.lruIndex["b"] = elemB resolver.lruIndex["c"] = elemC resolver.lruList.MoveToFront(resolver.lruIndex["a"]) order := []string{} for e := resolver.lruList.Back(); e != nil; e = e.Prev() { order = append(order, e.Value.(string)) } expected := []string{"b", "c", "a"} for i, v := range expected { if order[i] != v { t.Errorf("LRU order[%d] = %s, want %s", i, order[i], v) } } } // TestMockDNSNoopResolver 测试空实现解析器。 func TestMockDNSNoopResolver(t *testing.T) { var r Resolver = &noopResolver{} // LookupHost 应返回错误 _, err := r.LookupHost(context.Background(), "example.com") if err == nil { t.Error("Expected error from noopResolver.LookupHost") } // LookupHostWithCache 应返回错误 _, err = r.LookupHostWithCache(context.Background(), "example.com") if err == nil { t.Error("Expected error from noopResolver.LookupHostWithCache") } // Refresh 应返回 nil if err := r.Refresh("example.com"); err != nil { t.Errorf("noopResolver.Refresh() error: %v", err) } // Start 应返回 nil if err := r.Start(); err != nil { t.Errorf("noopResolver.Start() error: %v", err) } // Stop 应返回 nil if err := r.Stop(); err != nil { t.Errorf("noopResolver.Stop() error: %v", err) } // Stats 应返回空 stats := r.Stats() if stats.CacheHits != 0 || stats.CacheMisses != 0 { t.Error("noopResolver.Stats() should return zero values") } } // TestMockDNSCacheEntryConcurrency 测试缓存条目并发访问。 func TestMockDNSCacheEntryConcurrency(t *testing.T) { entry := &DNSCacheEntry{ IPs: []string{"127.0.0.1", "127.0.0.2"}, ExpiresAt: time.Now().Add(time.Hour), } var wg sync.WaitGroup var readCount atomic.Int64 // 并发读取 for range 100 { wg.Go(func() { entry.mu.RLock() _ = entry.IPs _ = entry.ExpiresAt entry.mu.RUnlock() readCount.Add(1) }) } wg.Wait() if readCount.Load() != 100 { t.Errorf("Expected 100 reads, got %d", readCount.Load()) } } // TestMockDNSIPv6QueryFallback 测试 IPv6 查询失败降级。 func TestMockDNSIPv6QueryFallback(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, IPv4: true, IPv6: true, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 查询 localhost(通常有 IPv4) ips, err := resolver.LookupHost(context.Background(), "localhost") if err != nil { t.Fatalf("LookupHost() error: %v", err) } // 应该至少有 IPv4 地址 if len(ips) == 0 { t.Error("Expected at least one IP address") } } // TestMockDNSDoRefresh 测试刷新操作。 func TestMockDNSDoRefresh(t *testing.T) { cfg := &config.ResolverConfig{ Enabled: true, Valid: 30 * time.Second, Timeout: 1 * time.Second, CacheSize: 10, } resolver := New(cfg).(*DNSResolver) // 添加需要刷新的主机 resolver.mu.Lock() resolver.refreshHosts["localhost"] = struct{}{} resolver.refreshHosts["127.0.0.1"] = struct{}{} // IP 地址会被跳过 resolver.mu.Unlock() // 执行刷新 resolver.doRefresh() // 验证刷新列表仍然存在 resolver.mu.RLock() _, exists := resolver.refreshHosts["localhost"] resolver.mu.RUnlock() if !exists { t.Error("Refresh hosts should still contain localhost") } }