test(resolver): 添加 DNS 解析器 Mock 测试
- 添加缓存、LRU 淘汰、刷新循环测试 - 添加并发访问、超时处理测试 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f91a40cc1d
commit
b34fae5885
565
internal/resolver/mock_dns_test.go
Normal file
565
internal/resolver/mock_dns_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user