package services import ( "context" "strings" "testing" "time" "github.com/Wikid82/charon/backend/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // setupTestDB creates a test database for DNS detection tests func setupTestDetectionDB(t *testing.T) *gorm.DB { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) // Migrate models err = db.AutoMigrate(&models.DNSProvider{}) require.NoError(t, err) return db } // seedTestProviders seeds the database with test DNS providers func seedTestProviders(t *testing.T, db *gorm.DB) { providers := []models.DNSProvider{ { UUID: "test-cloudflare-uuid", Name: "Production Cloudflare", ProviderType: "cloudflare", Enabled: true, IsDefault: true, PropagationTimeout: 120, PollingInterval: 5, KeyVersion: 1, }, { UUID: "test-route53-uuid", Name: "AWS Route53", ProviderType: "route53", Enabled: true, IsDefault: false, PropagationTimeout: 120, PollingInterval: 5, KeyVersion: 1, }, } for _, p := range providers { require.NoError(t, db.Create(&p).Error) } // Create disabled provider separately and explicitly disable it disabledProvider := models.DNSProvider{ UUID: "test-digitalocean-uuid", Name: "DigitalOcean DNS", ProviderType: "digitalocean", Enabled: true, // Create as enabled first IsDefault: false, PropagationTimeout: 120, PollingInterval: 5, KeyVersion: 1, } require.NoError(t, db.Create(&disabledProvider).Error) // Now explicitly disable it with an update require.NoError(t, db.Model(&disabledProvider).Update("enabled", false).Error) } func TestNewDNSDetectionService(t *testing.T) { db := setupTestDetectionDB(t) service := NewDNSDetectionService(db) assert.NotNil(t, service) // Verify it implements the interface by using it // NewDNSDetectionService returns DNSDetectionService, so type is guaranteed patterns := service.GetNameserverPatterns() assert.NotNil(t, patterns) } func TestGetNameserverPatterns(t *testing.T) { db := setupTestDetectionDB(t) service := NewDNSDetectionService(db) patterns := service.GetNameserverPatterns() // Verify we get expected providers assert.NotEmpty(t, patterns) assert.Contains(t, patterns, "cloudflare.com") assert.Equal(t, "cloudflare", patterns["cloudflare.com"]) assert.Contains(t, patterns, "awsdns") assert.Equal(t, "route53", patterns["awsdns"]) assert.Contains(t, patterns, "digitalocean.com") assert.Equal(t, "digitalocean", patterns["digitalocean.com"]) // Verify at least 10 providers assert.GreaterOrEqual(t, len(patterns), 10) } func TestMatchNameservers(t *testing.T) { db := setupTestDetectionDB(t) svc := NewDNSDetectionService(db).(*dnsDetectionService) tests := []struct { name string nameservers []string expectedType string expectedConf string }{ { name: "Cloudflare - high confidence", nameservers: []string{ "ns1.cloudflare.com", "ns2.cloudflare.com", }, expectedType: "cloudflare", expectedConf: "high", }, { name: "Route53 - high confidence", nameservers: []string{ "ns-123.awsdns-45.com", "ns-456.awsdns-78.net", }, expectedType: "route53", expectedConf: "high", }, { name: "DigitalOcean - high confidence", nameservers: []string{ "ns1.digitalocean.com", "ns2.digitalocean.com", "ns3.digitalocean.com", }, expectedType: "digitalocean", expectedConf: "high", }, { name: "Hetzner - high confidence", nameservers: []string{ "hydrogen.ns.hetzner.com", "oxygen.ns.hetzner.com", "helium.ns.hetzner.de", }, expectedType: "hetzner", expectedConf: "high", }, { name: "Mixed nameservers - medium confidence", nameservers: []string{ "ns1.cloudflare.com", "ns1.unknown-provider.com", }, expectedType: "cloudflare", expectedConf: "medium", }, { name: "Single match - low confidence", nameservers: []string{ "ns1.cloudflare.com", "ns1.unknown1.com", "ns2.unknown2.com", }, expectedType: "cloudflare", expectedConf: "low", }, { name: "No match", nameservers: []string{ "ns1.custom-provider.com", "ns2.custom-provider.com", }, expectedType: "", expectedConf: "none", }, { name: "Empty nameservers", nameservers: []string{}, expectedType: "", expectedConf: "none", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { providerType, confidence := svc.matchNameservers(tt.nameservers) assert.Equal(t, tt.expectedType, providerType, "Provider type mismatch") assert.Equal(t, tt.expectedConf, confidence, "Confidence mismatch") }) } } func TestDetectProvider_WithMockedDNS(t *testing.T) { db := setupTestDetectionDB(t) service := NewDNSDetectionService(db).(*dnsDetectionService) // Override resolver with mock that returns controlled results // In a real scenario, we'd use a mock resolver, but for this test // we'll test the logic without actual DNS lookups t.Run("handles wildcard domain", func(t *testing.T) { // We can't easily test actual DNS lookups in unit tests // But we can verify wildcard prefix removal domain := "*.example.com" result, err := service.DetectProvider(domain) require.NoError(t, err) assert.Equal(t, "example.com", result.Domain, "Wildcard prefix should be removed") }) t.Run("handles empty domain", func(t *testing.T) { result, err := service.DetectProvider("") require.NoError(t, err) assert.False(t, result.Detected) assert.Equal(t, "none", result.Confidence) assert.Equal(t, "invalid domain", result.Error) }) t.Run("normalizes domain", func(t *testing.T) { result, err := service.DetectProvider(" EXAMPLE.COM ") require.NoError(t, err) assert.Equal(t, "example.com", result.Domain, "Domain should be normalized") }) } func TestCaching(t *testing.T) { db := setupTestDetectionDB(t) service := NewDNSDetectionService(db).(*dnsDetectionService) // Manually cache a result testDomain := "test-cache.example.com" cachedResult := &DetectionResult{ Domain: testDomain, Detected: true, ProviderType: "cloudflare", Nameservers: []string{"ns1.cloudflare.com"}, Confidence: "high", } service.cacheResult(testDomain, cachedResult, 1*time.Hour) t.Run("retrieves cached result", func(t *testing.T) { result := service.getCachedResult(testDomain) require.NotNil(t, result) assert.Equal(t, testDomain, result.Domain) assert.True(t, result.Detected) assert.Equal(t, "cloudflare", result.ProviderType) }) t.Run("returns nil for non-existent cache", func(t *testing.T) { result := service.getCachedResult("non-existent-domain.com") assert.Nil(t, result) }) t.Run("expires old cache entries", func(t *testing.T) { expiredDomain := "expired.example.com" expiredResult := &DetectionResult{ Domain: expiredDomain, Detected: false, } // Cache with very short TTL service.cacheResult(expiredDomain, expiredResult, 1*time.Millisecond) // Wait for expiration time.Sleep(10 * time.Millisecond) result := service.getCachedResult(expiredDomain) assert.Nil(t, result, "Expired cache entry should return nil") }) } func TestSuggestConfiguredProvider(t *testing.T) { db := setupTestDetectionDB(t) seedTestProviders(t, db) service := NewDNSDetectionService(db).(*dnsDetectionService) // Mock a detection result by caching it cloudflareResult := &DetectionResult{ Domain: "cloudflare-example.com", Detected: true, ProviderType: "cloudflare", Nameservers: []string{"ns1.cloudflare.com"}, Confidence: "high", } service.cacheResult("cloudflare-example.com", cloudflareResult, 1*time.Hour) route53Result := &DetectionResult{ Domain: "route53-example.com", Detected: true, ProviderType: "route53", Nameservers: []string{"ns-123.awsdns-45.com"}, Confidence: "high", } service.cacheResult("route53-example.com", route53Result, 1*time.Hour) disabledResult := &DetectionResult{ Domain: "digitalocean-example.com", Detected: true, ProviderType: "digitalocean", Nameservers: []string{"ns1.digitalocean.com"}, Confidence: "high", } service.cacheResult("digitalocean-example.com", disabledResult, 1*time.Hour) unknownResult := &DetectionResult{ Domain: "unknown-example.com", Detected: true, ProviderType: "unknown-provider", Nameservers: []string{"ns1.unknown.com"}, Confidence: "high", } service.cacheResult("unknown-example.com", unknownResult, 1*time.Hour) ctx := context.Background() t.Run("suggests default cloudflare provider", func(t *testing.T) { provider, err := service.SuggestConfiguredProvider(ctx, "cloudflare-example.com") require.NoError(t, err) require.NotNil(t, provider) assert.Equal(t, "cloudflare", provider.ProviderType) assert.Equal(t, "Production Cloudflare", provider.Name) assert.True(t, provider.IsDefault) }) t.Run("suggests route53 provider", func(t *testing.T) { provider, err := service.SuggestConfiguredProvider(ctx, "route53-example.com") require.NoError(t, err) require.NotNil(t, provider) assert.Equal(t, "route53", provider.ProviderType) assert.Equal(t, "AWS Route53", provider.Name) }) t.Run("returns nil for disabled provider", func(t *testing.T) { provider, err := service.SuggestConfiguredProvider(ctx, "digitalocean-example.com") require.NoError(t, err) assert.Nil(t, provider, "Should not suggest disabled provider") }) t.Run("returns nil for unknown provider", func(t *testing.T) { provider, err := service.SuggestConfiguredProvider(ctx, "unknown-example.com") require.NoError(t, err) assert.Nil(t, provider, "Should not suggest non-existent provider type") }) } func TestDetectionResult_Validation(t *testing.T) { t.Run("result with all fields", func(t *testing.T) { result := &DetectionResult{ Domain: "example.com", Detected: true, ProviderType: "cloudflare", Nameservers: []string{"ns1.cloudflare.com", "ns2.cloudflare.com"}, Confidence: "high", } assert.Equal(t, "example.com", result.Domain) assert.True(t, result.Detected) assert.Equal(t, "cloudflare", result.ProviderType) assert.Len(t, result.Nameservers, 2) assert.Equal(t, "high", result.Confidence) assert.Empty(t, result.Error) }) t.Run("result with error", func(t *testing.T) { result := &DetectionResult{ Detected: false, Confidence: "none", Error: "DNS lookup failed: no such host", } assert.False(t, result.Detected) assert.Equal(t, "none", result.Confidence) assert.NotEmpty(t, result.Error) }) } func TestBuiltInNameserversCompleteness(t *testing.T) { // Verify that built-in nameservers cover expected providers expectedProviders := []string{ "cloudflare", "route53", "digitalocean", "googleclouddns", "azure", "namecheap", "godaddy", "hetzner", "vultr", "dnsimple", } foundProviders := make(map[string]bool) for _, providerType := range BuiltInNameservers { foundProviders[providerType] = true } for _, expected := range expectedProviders { assert.True(t, foundProviders[expected], "Missing provider: %s", expected) } // Verify at least 10 unique providers assert.GreaterOrEqual(t, len(foundProviders), 10, "Should have at least 10 providers") } func TestCaseInsensitiveMatching(t *testing.T) { db := setupTestDetectionDB(t) svc := NewDNSDetectionService(db).(*dnsDetectionService) tests := []struct { name string nameservers []string expected string }{ { name: "lowercase", nameservers: []string{"ns1.cloudflare.com"}, expected: "cloudflare", }, { name: "uppercase", nameservers: []string{"NS1.CLOUDFLARE.COM"}, expected: "cloudflare", }, { name: "mixed case", nameservers: []string{"Ns1.CloudFlare.Com"}, expected: "cloudflare", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { providerType, _ := svc.matchNameservers(tt.nameservers) assert.Equal(t, tt.expected, providerType) }) } } func TestConcurrentCacheAccess(t *testing.T) { db := setupTestDetectionDB(t) service := NewDNSDetectionService(db).(*dnsDetectionService) // Test concurrent cache writes and reads done := make(chan bool) const goroutines = 10 for i := 0; i < goroutines; i++ { go func(id int) { domain := strings.ReplaceAll("test-DOMAIN-ID.com", "ID", string(rune(id))) result := &DetectionResult{ Domain: domain, Detected: true, } // Write service.cacheResult(domain, result, 1*time.Hour) // Read cached := service.getCachedResult(domain) assert.NotNil(t, cached) done <- true }(i) } // Wait for all goroutines for i := 0; i < goroutines; i++ { <-done } } // TestDatabaseError verifies error handling when database is unavailable func TestDatabaseError(t *testing.T) { // Create a database and immediately close it db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) sqlDB, err := db.DB() require.NoError(t, err) _ = sqlDB.Close() service := NewDNSDetectionService(db) // Cache a successful detection svc := service.(*dnsDetectionService) testResult := &DetectionResult{ Domain: "test.com", Detected: true, ProviderType: "cloudflare", Nameservers: []string{"ns1.cloudflare.com"}, Confidence: "high", } svc.cacheResult("test.com", testResult, 1*time.Hour) // This should fail because database is closed provider, err := service.SuggestConfiguredProvider(context.Background(), "test.com") // Should get error due to closed database assert.Error(t, err) assert.Nil(t, provider) // Check for database closed error (exact message varies by SQLite driver) assert.Contains(t, err.Error(), "database is closed") }