# Test Coverage Remediation Plan **Date:** January 3, 2026 **Current Patch Coverage:** 84.85% **Target:** ≥85% **Missing Lines:** 134 total --- ## Executive Summary This plan details the specific test cases needed to increase patch coverage from 84.85% to 85%+. The analysis identified uncovered code paths in 10 files and provides implementation-ready test specifications for Backend_Dev and Frontend_Dev agents. --- ## Phase 1: Quick Wins (Estimated +22-24 lines) ### 1.1 `backend/internal/network/internal_service_client.go` — 0% → 100% **Test File:** `backend/internal/network/internal_service_client_test.go` (CREATE NEW) **Uncovered:** Entire file (14 lines) ```go package network import ( "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewInternalServiceHTTPClient_CreatesClientWithCorrectTimeout(t *testing.T) { timeout := 5 * time.Second client := NewInternalServiceHTTPClient(timeout) require.NotNil(t, client) assert.Equal(t, timeout, client.Timeout) } func TestNewInternalServiceHTTPClient_TransportSettings(t *testing.T) { client := NewInternalServiceHTTPClient(10 * time.Second) transport, ok := client.Transport.(*http.Transport) require.True(t, ok, "Transport should be *http.Transport") // Verify SSRF-safe settings assert.Nil(t, transport.Proxy, "Proxy should be nil to ignore env vars") assert.True(t, transport.DisableKeepAlives, "KeepAlives should be disabled") assert.Equal(t, 1, transport.MaxIdleConns) } func TestNewInternalServiceHTTPClient_DisablesRedirects(t *testing.T) { // Server that redirects server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.Redirect(w, r, "/other", http.StatusFound) return } w.WriteHeader(http.StatusOK) })) defer server.Close() client := NewInternalServiceHTTPClient(5 * time.Second) resp, err := client.Get(server.URL) require.NoError(t, err) defer resp.Body.Close() // Should NOT follow redirect - returns the redirect response directly assert.Equal(t, http.StatusFound, resp.StatusCode) } func TestNewInternalServiceHTTPClient_RespectsTimeout(t *testing.T) { // Server that delays response server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(500 * time.Millisecond) w.WriteHeader(http.StatusOK) })) defer server.Close() // Very short timeout client := NewInternalServiceHTTPClient(50 * time.Millisecond) _, err := client.Get(server.URL) require.Error(t, err) assert.Contains(t, err.Error(), "timeout") } ``` **Coverage Gain:** +14 lines --- ### 1.2 `backend/internal/crypto/encryption.go` — 74.35% → ~90% **Test File:** `backend/internal/crypto/encryption_test.go` (EXTEND) **Uncovered Code Paths:** - Lines 35-37: `aes.NewCipher` error (difficult to trigger) - Lines 38-40: `cipher.NewGCM` error (difficult to trigger) - Lines 43-45: `io.ReadFull(rand.Reader, nonce)` error ```go // ADD to existing encryption_test.go func TestEncrypt_NilPlaintext(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) keyBase64 := base64.StdEncoding.EncodeToString(key) svc, err := NewEncryptionService(keyBase64) require.NoError(t, err) // Encrypting nil should work (treated as empty) ciphertext, err := svc.Encrypt(nil) assert.NoError(t, err) assert.NotEmpty(t, ciphertext) // Should decrypt to empty decrypted, err := svc.Decrypt(ciphertext) assert.NoError(t, err) assert.Empty(t, decrypted) } func TestDecrypt_ExactlyNonceSizeBytes(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) keyBase64 := base64.StdEncoding.EncodeToString(key) svc, err := NewEncryptionService(keyBase64) require.NoError(t, err) // Create ciphertext that is exactly nonce size (12 bytes for GCM) // This should fail because there's no actual ciphertext after the nonce shortCiphertext := base64.StdEncoding.EncodeToString(make([]byte, 12)) _, err = svc.Decrypt(shortCiphertext) assert.Error(t, err) assert.Contains(t, err.Error(), "decryption failed") } func TestEncryptDecrypt_LargeData(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) keyBase64 := base64.StdEncoding.EncodeToString(key) svc, err := NewEncryptionService(keyBase64) require.NoError(t, err) // Test with 1MB of data largeData := make([]byte, 1024*1024) _, _ = rand.Read(largeData) ciphertext, err := svc.Encrypt(largeData) require.NoError(t, err) decrypted, err := svc.Decrypt(ciphertext) require.NoError(t, err) assert.Equal(t, largeData, decrypted) } ``` **Coverage Gain:** +8-10 lines --- ## Phase 2: High Impact (Estimated +30-38 lines) ### 2.1 `backend/internal/utils/url_testing.go` — 74.83% → ~90% **Test File:** `backend/internal/utils/url_testing_coverage_test.go` (CREATE NEW) **Uncovered Code Paths:** 1. `resolveAllowedIP`: IP literal localhost allowed path 2. `resolveAllowedIP`: DNS returning empty IPs 3. `resolveAllowedIP`: Multiple IPs with first being loopback 4. `testURLConnectivity`: Error message transformations 5. `testURLConnectivity`: Port validation paths 6. `validateRedirectTargetStrict`: Scheme downgrade blocking 7. `validateRedirectTargetStrict`: Max redirects exceeded ```go package utils import ( "context" "net" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============== resolveAllowedIP Coverage ============== func TestResolveAllowedIP_IPLiteralLocalhostAllowed(t *testing.T) { ctx := context.Background() // With allowLocalhost=true, loopback should be allowed ip, err := resolveAllowedIP(ctx, "127.0.0.1", true) require.NoError(t, err) assert.True(t, ip.IsLoopback()) } func TestResolveAllowedIP_EmptyHostname(t *testing.T) { ctx := context.Background() _, err := resolveAllowedIP(ctx, "", false) require.Error(t, err) assert.Contains(t, err.Error(), "missing hostname") } func TestResolveAllowedIP_PrivateIPBlocked(t *testing.T) { ctx := context.Background() // IP literal in private range _, err := resolveAllowedIP(ctx, "192.168.1.1", false) require.Error(t, err) assert.Contains(t, err.Error(), "private IP") } func TestResolveAllowedIP_PublicIPAllowed(t *testing.T) { ctx := context.Background() // Public IP literal ip, err := resolveAllowedIP(ctx, "8.8.8.8", false) require.NoError(t, err) assert.Equal(t, "8.8.8.8", ip.String()) } // ============== validateRedirectTargetStrict Coverage ============== func TestValidateRedirectTarget_SchemeDowngradeBlocked(t *testing.T) { // Previous request was HTTPS prevReq, _ := http.NewRequest(http.MethodGet, "https://example.com", nil) // New request is HTTP (downgrade) newReq, _ := http.NewRequest(http.MethodGet, "http://example.com/path", nil) err := validateRedirectTargetStrict(newReq, []*http.Request{prevReq}, 5, false, false) require.Error(t, err) assert.Contains(t, err.Error(), "scheme change blocked") } func TestValidateRedirectTarget_HTTPSUpgradeAllowed(t *testing.T) { // Previous request was HTTP prevReq, _ := http.NewRequest(http.MethodGet, "http://localhost", nil) // New request is HTTPS (upgrade) - should be allowed when allowHTTPSUpgrade=true newReq, _ := http.NewRequest(http.MethodGet, "https://localhost/path", nil) err := validateRedirectTargetStrict(newReq, []*http.Request{prevReq}, 5, true, true) // May fail on security validation, but not on scheme change if err != nil { assert.NotContains(t, err.Error(), "scheme change blocked") } } func TestValidateRedirectTarget_MaxRedirectsExceeded(t *testing.T) { // Create via slice with maxRedirects entries via := make([]*http.Request, 3) for i := range via { via[i], _ = http.NewRequest(http.MethodGet, "http://example.com", nil) } newReq, _ := http.NewRequest(http.MethodGet, "http://example.com/final", nil) err := validateRedirectTargetStrict(newReq, via, 3, true, true) require.Error(t, err) assert.Contains(t, err.Error(), "too many redirects") } // ============== testURLConnectivity Coverage ============== func TestURLConnectivity_InvalidPortNumber(t *testing.T) { // Port 0 should be rejected reachable, _, err := testURLConnectivity( "https://example.com:0/path", withAllowLocalhostForTesting(), ) require.Error(t, err) assert.False(t, reachable) } func TestURLConnectivity_PortOutOfRange(t *testing.T) { // Port > 65535 should be rejected reachable, _, err := testURLConnectivity( "https://example.com:70000/path", withAllowLocalhostForTesting(), ) require.Error(t, err) assert.False(t, reachable) assert.Contains(t, err.Error(), "invalid") } func TestURLConnectivity_ServerError5xx(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() reachable, latency, err := testURLConnectivity( server.URL, withAllowLocalhostForTesting(), withTransportForTesting(server.Client().Transport), ) require.Error(t, err) assert.False(t, reachable) assert.Greater(t, latency, float64(0)) assert.Contains(t, err.Error(), "status 500") } ``` **Coverage Gain:** +15-20 lines --- ### 2.2 `backend/internal/services/dns_provider_service.go` — 78.26% → ~90% **Test File:** `backend/internal/services/dns_provider_service_test.go` (EXTEND) **Uncovered Code Paths:** 1. `Create`: DB error during default provider update 2. `Update`: Explicit IsDefault=false unsetting 3. `Update`: DB error during save 4. `Test`: Decryption failure path (already tested, verify) 5. `testDNSProviderCredentials`: Validation failure ```go // ADD to existing dns_provider_service_test.go func TestDNSProviderService_Update_ExplicitUnsetDefault(t *testing.T) { db, encryptor := setupDNSProviderTestDB(t) service := NewDNSProviderService(db, encryptor) ctx := context.Background() // Create provider as default provider, err := service.Create(ctx, CreateDNSProviderRequest{ Name: "Default Provider", ProviderType: "cloudflare", Credentials: map[string]string{"api_token": "token"}, IsDefault: true, }) require.NoError(t, err) assert.True(t, provider.IsDefault) // Explicitly unset default notDefault := false updated, err := service.Update(ctx, provider.ID, UpdateDNSProviderRequest{ IsDefault: ¬Default, }) require.NoError(t, err) assert.False(t, updated.IsDefault) } func TestDNSProviderService_Update_AllFieldsAtOnce(t *testing.T) { db, encryptor := setupDNSProviderTestDB(t) service := NewDNSProviderService(db, encryptor) ctx := context.Background() // Create initial provider provider, err := service.Create(ctx, CreateDNSProviderRequest{ Name: "Original", ProviderType: "cloudflare", Credentials: map[string]string{"api_token": "original"}, PropagationTimeout: 60, PollingInterval: 2, }) require.NoError(t, err) // Update all fields at once newName := "Updated Name" newTimeout := 180 newInterval := 10 newEnabled := false newDefault := true newCreds := map[string]string{"api_token": "new-token"} updated, err := service.Update(ctx, provider.ID, UpdateDNSProviderRequest{ Name: &newName, PropagationTimeout: &newTimeout, PollingInterval: &newInterval, Enabled: &newEnabled, IsDefault: &newDefault, Credentials: newCreds, }) require.NoError(t, err) assert.Equal(t, "Updated Name", updated.Name) assert.Equal(t, 180, updated.PropagationTimeout) assert.Equal(t, 10, updated.PollingInterval) assert.False(t, updated.Enabled) assert.True(t, updated.IsDefault) } func TestDNSProviderService_Test_UpdatesFailureStatistics(t *testing.T) { db, encryptor := setupDNSProviderTestDB(t) service := NewDNSProviderService(db, encryptor) ctx := context.Background() // Create provider with invalid encrypted credentials provider := &models.DNSProvider{ UUID: "test-uuid", Name: "Test", ProviderType: "cloudflare", CredentialsEncrypted: "invalid-ciphertext", Enabled: true, } require.NoError(t, db.Create(provider).Error) // Test should fail decryption and update failure statistics result, err := service.Test(ctx, provider.ID) require.NoError(t, err) // No error returned, but result indicates failure assert.False(t, result.Success) assert.Equal(t, "DECRYPTION_ERROR", result.Code) // Verify failure count was incremented var updatedProvider models.DNSProvider require.NoError(t, db.First(&updatedProvider, provider.ID).Error) assert.Equal(t, 1, updatedProvider.FailureCount) assert.NotEmpty(t, updatedProvider.LastError) } func TestTestDNSProviderCredentials_MissingField(t *testing.T) { // Test with missing required field result := testDNSProviderCredentials("route53", map[string]string{ "access_key_id": "key", // Missing secret_access_key and region }) assert.False(t, result.Success) assert.Equal(t, "VALIDATION_ERROR", result.Code) assert.Contains(t, result.Error, "missing") } func TestDNSProviderService_Create_SetsDefaults(t *testing.T) { db, encryptor := setupDNSProviderTestDB(t) service := NewDNSProviderService(db, encryptor) ctx := context.Background() // Create without specifying timeout/interval provider, err := service.Create(ctx, CreateDNSProviderRequest{ Name: "Default Test", ProviderType: "cloudflare", Credentials: map[string]string{"api_token": "token"}, // PropagationTimeout and PollingInterval not set }) require.NoError(t, err) // Should have default values assert.Equal(t, 120, provider.PropagationTimeout) assert.Equal(t, 5, provider.PollingInterval) assert.True(t, provider.Enabled) // Default enabled } func TestDNSProviderService_GetDecryptedCredentials_UpdatesLastUsed(t *testing.T) { db, encryptor := setupDNSProviderTestDB(t) service := NewDNSProviderService(db, encryptor) ctx := context.Background() // Create provider provider, err := service.Create(ctx, CreateDNSProviderRequest{ Name: "Test", ProviderType: "cloudflare", Credentials: map[string]string{"api_token": "token"}, }) require.NoError(t, err) // Initially no last_used_at assert.Nil(t, provider.LastUsedAt) // Get decrypted credentials _, err = service.GetDecryptedCredentials(ctx, provider.ID) require.NoError(t, err) // Verify last_used_at was updated var updatedProvider models.DNSProvider require.NoError(t, db.First(&updatedProvider, provider.ID).Error) assert.NotNil(t, updatedProvider.LastUsedAt) } ``` **Coverage Gain:** +15-18 lines --- ## Phase 3: Medium Impact (Estimated +14-18 lines) ### 3.1 `backend/internal/security/url_validator.go` — 77.55% → ~88% **Test File:** `backend/internal/security/url_validator_test.go` (EXTEND) **Uncovered Code Paths:** 1. `ValidateInternalServiceBaseURL`: All error paths 2. `ParseExactHostnameAllowlist`: Invalid hostname filtering ```go // ADD to existing url_validator_test.go or internal_service_url_validator_test.go func TestValidateInternalServiceBaseURL_AllErrorPaths(t *testing.T) { allowedHosts := map[string]struct{}{ "localhost": {}, "127.0.0.1": {}, } tests := []struct { name string url string port int errContains string }{ { name: "invalid URL format", url: "://invalid", port: 8080, errContains: "invalid url format", }, { name: "unsupported scheme", url: "ftp://localhost:8080", port: 8080, errContains: "unsupported scheme", }, { name: "embedded credentials", url: "http://user:pass@localhost:8080", port: 8080, errContains: "embedded credentials", }, { name: "missing hostname", url: "http:///path", port: 8080, errContains: "missing hostname", }, { name: "hostname not allowed", url: "http://evil.com:8080", port: 8080, errContains: "hostname not allowed", }, { name: "invalid port", url: "http://localhost:abc", port: 8080, errContains: "invalid port", }, { name: "port mismatch", url: "http://localhost:9090", port: 8080, errContains: "unexpected port", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := ValidateInternalServiceBaseURL(tt.url, tt.port, allowedHosts) require.Error(t, err) assert.Contains(t, strings.ToLower(err.Error()), tt.errContains) }) } } func TestValidateInternalServiceBaseURL_Success(t *testing.T) { allowedHosts := map[string]struct{}{ "localhost": {}, "crowdsec": {}, } tests := []struct { name string url string port int }{ {"HTTP localhost", "http://localhost:8080", 8080}, {"HTTPS localhost", "https://localhost:443", 443}, {"Service name", "http://crowdsec:8085", 8085}, {"Default HTTP port", "http://localhost", 80}, {"Default HTTPS port", "https://localhost", 443}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ValidateInternalServiceBaseURL(tt.url, tt.port, allowedHosts) require.NoError(t, err) require.NotNil(t, result) }) } } func TestParseExactHostnameAllowlist_FiltersInvalidEntries(t *testing.T) { tests := []struct { name string input string expected map[string]struct{} }{ { name: "valid entries", input: "localhost,crowdsec,caddy", expected: map[string]struct{}{ "localhost": {}, "crowdsec": {}, "caddy": {}, }, }, { name: "filters entries with scheme", input: "localhost,http://invalid,crowdsec", expected: map[string]struct{}{ "localhost": {}, "crowdsec": {}, }, }, { name: "filters entries with @", input: "localhost,user@host,crowdsec", expected: map[string]struct{}{ "localhost": {}, "crowdsec": {}, }, }, { name: "empty string", input: "", expected: map[string]struct{}{}, }, { name: "handles whitespace", input: " localhost , crowdsec ", expected: map[string]struct{}{ "localhost": {}, "crowdsec": {}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ParseExactHostnameAllowlist(tt.input) assert.Equal(t, tt.expected, result) }) } } ``` **Coverage Gain:** +8-10 lines --- ### 3.2 `backend/internal/services/notification_service.go` — 66.66% → ~82% **Test File:** `backend/internal/services/notification_service_test.go` (CREATE OR EXTEND) **Uncovered Code Paths:** 1. `sendJSONPayload`: Template size limit exceeded 2. `sendJSONPayload`: Discord/Slack/Gotify validation 3. `sendJSONPayload`: DNS resolution failure 4. `SendExternal`: Event type filtering ```go // File: backend/internal/services/notification_service_test.go package services import ( "context" "strings" "testing" "github.com/Wikid82/charon/backend/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func setupNotificationTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}, &models.NotificationTemplate{})) return db } func TestSendJSONPayload_TemplateSizeExceeded(t *testing.T) { db := setupNotificationTestDB(t) svc := NewNotificationService(db) // Template larger than 10KB limit largeTemplate := strings.Repeat("x", 11*1024) provider := models.NotificationProvider{ Name: "Test", Type: "webhook", URL: "https://example.com/webhook", Template: "custom", Config: largeTemplate, } err := svc.sendJSONPayload(context.Background(), provider, map[string]any{}) require.Error(t, err) assert.Contains(t, err.Error(), "exceeds maximum limit") } func TestSendJSONPayload_DiscordValidation(t *testing.T) { db := setupNotificationTestDB(t) svc := NewNotificationService(db) // Discord requires 'content' or 'embeds' provider := models.NotificationProvider{ Name: "Discord", Type: "discord", URL: "https://discord.com/api/webhooks/123/abc", Template: "custom", Config: `{"message": "test"}`, // Missing 'content' or 'embeds' } err := svc.sendJSONPayload(context.Background(), provider, map[string]any{ "Message": "test", }) require.Error(t, err) assert.Contains(t, err.Error(), "content") } func TestSendJSONPayload_SlackValidation(t *testing.T) { db := setupNotificationTestDB(t) svc := NewNotificationService(db) // Slack requires 'text' or 'blocks' provider := models.NotificationProvider{ Name: "Slack", Type: "slack", URL: "https://hooks.slack.com/services/T00/B00/xxx", Template: "custom", Config: `{"message": "test"}`, // Missing 'text' or 'blocks' } err := svc.sendJSONPayload(context.Background(), provider, map[string]any{ "Message": "test", }) require.Error(t, err) assert.Contains(t, err.Error(), "text") } func TestSendExternal_EventTypeFiltering(t *testing.T) { db := setupNotificationTestDB(t) svc := NewNotificationService(db) // Create provider that only notifies on 'uptime' events provider := &models.NotificationProvider{ Name: "Uptime Only", Type: "webhook", URL: "https://example.com/webhook", Enabled: true, NotifyUptime: true, NotifyDomains: false, NotifyCerts: false, } require.NoError(t, db.Create(provider).Error) // Test that non-uptime events are filtered (no actual HTTP call made due to filtering) // This tests the shouldSend logic svc.SendExternal(context.Background(), "domain", "Test", "Test message", nil) // If we get here without panic/error, filtering works // (In real test, we'd mock the HTTP client and verify no call was made) } ``` **Coverage Gain:** +6-8 lines --- ## Phase 4: Cleanup (Estimated +10-14 lines) ### 4.1 `backend/internal/api/handlers/crowdsec_handler.go` — 82.85% → ~88% **Test File:** `backend/internal/api/handlers/crowdsec_handler_test.go` (EXTEND) **Uncovered:** 1. `GetLAPIDecisions`: Non-JSON content-type fallback 2. `CheckLAPIHealth`: Fallback to decisions endpoint ```go // ADD to crowdsec_handler_test.go func TestGetLAPIDecisions_NonJSONContentType(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) // Mock server that returns HTML instead of JSON server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte("Not JSON")) })) defer server.Close() // This test verifies the content-type check path // The handler should fall back to cscli method // ... test implementation } func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) // Mock server where /health fails but /v1/decisions returns 401 callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ if r.URL.Path == "/health" { w.WriteHeader(http.StatusNotFound) return } if r.URL.Path == "/v1/decisions" { w.WriteHeader(http.StatusUnauthorized) // Expected without auth return } })) defer server.Close() // Test verifies the fallback logic // ... test implementation } func TestValidateCrowdsecLAPIBaseURL_InvalidURL(t *testing.T) { _, err := validateCrowdsecLAPIBaseURL("invalid://url") require.Error(t, err) } ``` **Coverage Gain:** +5-6 lines --- ### 4.2 `backend/internal/api/handlers/dns_provider_handler.go` — 98.30% → 100% **Test File:** `backend/internal/api/handlers/dns_provider_handler_test.go` (EXTEND) ```go // ADD to existing test file func TestDNSProviderHandler_InvalidIDParameter(t *testing.T) { // Test with non-numeric ID // ... test implementation } func TestDNSProviderHandler_ServiceError(t *testing.T) { // Test handler error response when service returns error // ... test implementation } ``` **Coverage Gain:** +4-5 lines --- ### 4.3 `backend/internal/services/uptime_service.go` — 85.71% → 88% **Minimal remaining uncovered paths - low priority** --- ### 4.4 Frontend: `DNSProviderSelector.tsx` — 86.36% → 100% **Test File:** `frontend/src/components/__tests__/DNSProviderSelector.test.tsx` (CREATE) ```tsx import { render, screen } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import DNSProviderSelector from '../DNSProviderSelector' // Mock the hook jest.mock('../../hooks/useDNSProviders', () => ({ useDNSProviders: jest.fn(), })) const { useDNSProviders } = require('../../hooks/useDNSProviders') const wrapper = ({ children }) => ( {children} ) describe('DNSProviderSelector', () => { it('renders loading state', () => { useDNSProviders.mockReturnValue({ data: [], isLoading: true }) render( {}} />, { wrapper }) expect(screen.getByText(/loading/i)).toBeInTheDocument() }) it('renders empty state when no providers', () => { useDNSProviders.mockReturnValue({ data: [], isLoading: false }) render( {}} />, { wrapper }) // Open the select // Verify empty state message }) it('displays error message when provided', () => { useDNSProviders.mockReturnValue({ data: [], isLoading: false }) render( {}} error="Provider is required" />, { wrapper } ) expect(screen.getByRole('alert')).toHaveTextContent('Provider is required') }) }) ``` **Coverage Gain:** +3 lines --- ## Summary: Estimated Coverage Impact | Phase | Files | Est. Lines Covered | Priority | |-------|-------|-------------------|----------| | Phase 1 | internal_service_client, encryption | +22-24 | IMMEDIATE | | Phase 2 | url_testing, dns_provider_service | +30-38 | HIGH | | Phase 3 | url_validator, notification_service | +14-18 | MEDIUM | | Phase 4 | crowdsec_handler, dns_provider_handler, frontend | +10-14 | LOW | **Total Estimated:** +76-94 lines covered **Projected Patch Coverage:** 84.85% + ~7-8% = **91-93%** --- ## Verification Commands ```bash # Run all backend tests with coverage cd backend && go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | tail -20 # Check specific package coverage go test -coverprofile=cover.out ./internal/network/... && go tool cover -func=cover.out # Generate HTML report go tool cover -html=coverage.out -o coverage.html # Frontend coverage cd frontend && npm run test -- --coverage --watchAll=false ``` --- ## Definition of Done - [ ] All new test files created - [ ] All test cases implemented - [ ] `go test ./...` passes - [ ] Coverage report shows ≥85% patch coverage - [ ] No linter warnings in new test code - [ ] Pre-commit hooks pass