test(resolver): 添加 DNS 解析器 Mock 测试

- 添加缓存、LRU 淘汰、刷新循环测试
- 添加并发访问、超时处理测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-23 17:20:32 +08:00
parent f91a40cc1d
commit b34fae5885

View File

@ -0,0 +1,565 @@
// 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 i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
_, _ = 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": {},
}
resolver.lruOrder = []string{"a", "b", "c"}
// 移动 "a" 到前端
resolver.moveToFrontLocked("a")
// 验证顺序
expected := []string{"b", "c", "a"}
for i, v := range expected {
if resolver.lruOrder[i] != v {
t.Errorf("LRU order[%d] = %s, want %s", i, resolver.lruOrder[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 i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
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")
}
}