package integration import ( "context" "os" "testing" "time" "chorus.services/bzzz/pkg/config" "chorus.services/bzzz/pkg/dht" ) // Phase 2 Hybrid DHT Integration Tests // These tests validate the hybrid DHT system's ability to switch between mock and real backends func TestPhase2HybridDHTBasic(t *testing.T) { ctx := context.Background() t.Run("Hybrid_DHT_Creation", func(t *testing.T) { testHybridDHTCreation(t, ctx) }) t.Run("Mock_Backend_Operations", func(t *testing.T) { testMockBackendOperations(t, ctx) }) t.Run("Backend_Switching", func(t *testing.T) { testBackendSwitching(t, ctx) }) t.Run("Health_Monitoring", func(t *testing.T) { testHealthMonitoring(t, ctx) }) t.Run("Metrics_Collection", func(t *testing.T) { testMetricsCollection(t, ctx) }) } func testHybridDHTCreation(t *testing.T, ctx context.Context) { // Create configuration for mock-only mode config := &config.HybridConfig{ DHT: config.HybridDHTConfig{ Backend: "mock", FallbackOnError: true, HealthCheckInterval: 5 * time.Second, MaxRetries: 3, OperationTimeout: 10 * time.Second, }, Monitoring: config.MonitoringConfig{ Enabled: true, MetricsInterval: 15 * time.Second, }, } logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT: %v", err) } defer hybridDHT.Close() // Verify initial state health := hybridDHT.GetBackendHealth() if mockHealth, exists := health["mock"]; exists { if mockHealth.Status != dht.HealthStatusHealthy { t.Errorf("Expected mock backend to be healthy, got %v", mockHealth.Status) } } else { t.Error("Mock backend health not found") } t.Logf("✓ Hybrid DHT created successfully in mock mode") } func testMockBackendOperations(t *testing.T, ctx context.Context) { config := &config.HybridConfig{ DHT: config.HybridDHTConfig{ Backend: "mock", FallbackOnError: true, HealthCheckInterval: 5 * time.Second, }, } logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT: %v", err) } defer hybridDHT.Close() // Test basic operations testKey := "phase2-test-key" testValue := []byte("phase2-test-value") // Store value err = hybridDHT.PutValue(ctx, testKey, testValue) if err != nil { t.Fatalf("Failed to put value: %v", err) } // Retrieve value retrievedValue, err := hybridDHT.GetValue(ctx, testKey) if err != nil { t.Fatalf("Failed to get value: %v", err) } if string(retrievedValue) != string(testValue) { t.Errorf("Retrieved value doesn't match: got %s, want %s", retrievedValue, testValue) } // Test provider operations providerId := "phase2-provider-001" err = hybridDHT.Provide(ctx, testKey, providerId) if err != nil { t.Fatalf("Failed to provide: %v", err) } providers, err := hybridDHT.FindProviders(ctx, testKey) if err != nil { t.Fatalf("Failed to find providers: %v", err) } found := false for _, p := range providers { if p == providerId { found = true break } } if !found { t.Errorf("Provider %s not found in provider list", providerId) } // Check metrics metrics := hybridDHT.GetHybridMetrics() if metrics.MockRequests == 0 { t.Error("Expected mock requests to be > 0") } if metrics.RealRequests != 0 { t.Error("Expected real requests to be 0 in mock mode") } t.Logf("✓ Mock backend operations working correctly") t.Logf(" - Mock requests: %d", metrics.MockRequests) t.Logf(" - Real requests: %d", metrics.RealRequests) } func testBackendSwitching(t *testing.T, ctx context.Context) { config := &config.HybridConfig{ DHT: config.HybridDHTConfig{ Backend: "mock", FallbackOnError: true, HealthCheckInterval: 5 * time.Second, }, } logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT: %v", err) } defer hybridDHT.Close() // Verify starting with mock backend initialMetrics := hybridDHT.GetHybridMetrics() if initialMetrics.MockRequests != 0 || initialMetrics.RealRequests != 0 { t.Error("Expected initial metrics to be zero") } // Perform operation with mock testKey := "switching-test-key" testValue := []byte("switching-test-value") err = hybridDHT.PutValue(ctx, testKey, testValue) if err != nil { t.Fatalf("Failed to put value with mock backend: %v", err) } // Verify mock was used afterMetrics := hybridDHT.GetHybridMetrics() if afterMetrics.MockRequests == 0 { t.Error("Expected mock requests to be > 0") } if afterMetrics.RealRequests != 0 { t.Error("Expected real requests to be 0") } // Test manual backend switching (should succeed for mock) err = hybridDHT.SwitchBackend("mock") if err != nil { t.Errorf("Failed to switch to mock backend: %v", err) } // Test switching to non-existent real backend (should fail) err = hybridDHT.SwitchBackend("real") if err == nil { t.Error("Expected error when switching to unavailable real backend") } t.Logf("✓ Backend switching mechanism working correctly") } func testHealthMonitoring(t *testing.T, ctx context.Context) { config := &config.HybridConfig{ DHT: config.HybridDHTConfig{ Backend: "mock", FallbackOnError: true, HealthCheckInterval: 1 * time.Second, // Fast for testing }, Monitoring: config.MonitoringConfig{ Enabled: true, MetricsInterval: 1 * time.Second, // Fast for testing }, } logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT: %v", err) } defer hybridDHT.Close() // Wait for initial health check time.Sleep(100 * time.Millisecond) // Check initial health status health := hybridDHT.GetBackendHealth() if mockHealth, exists := health["mock"]; exists { if mockHealth.Status != dht.HealthStatusHealthy { t.Errorf("Expected mock backend to be healthy, got %v", mockHealth.Status) } if mockHealth.ErrorCount != 0 { t.Errorf("Expected no errors initially, got %d", mockHealth.ErrorCount) } } else { t.Error("Mock backend health not found") } // Wait for health monitoring to run time.Sleep(1200 * time.Millisecond) // Verify health monitoring is working healthAfter := hybridDHT.GetBackendHealth() if mockHealthAfter, exists := healthAfter["mock"]; exists { if mockHealthAfter.Status != dht.HealthStatusHealthy { t.Errorf("Expected mock backend to remain healthy, got %v", mockHealthAfter.Status) } } t.Logf("✓ Health monitoring system working correctly") } func testMetricsCollection(t *testing.T, ctx context.Context) { config := &config.HybridConfig{ DHT: config.HybridDHTConfig{ Backend: "mock", FallbackOnError: true, HealthCheckInterval: 5 * time.Second, }, Monitoring: config.MonitoringConfig{ Enabled: true, MetricsInterval: 1 * time.Second, }, } logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT: %v", err) } defer hybridDHT.Close() // Perform multiple operations for i := 0; i < 5; i++ { key := fmt.Sprintf("metrics-test-key-%d", i) value := []byte(fmt.Sprintf("metrics-test-value-%d", i)) err = hybridDHT.PutValue(ctx, key, value) if err != nil { t.Fatalf("Failed to put value %d: %v", i, err) } retrievedValue, err := hybridDHT.GetValue(ctx, key) if err != nil { t.Fatalf("Failed to get value %d: %v", i, err) } if string(retrievedValue) != string(value) { t.Errorf("Retrieved value %d doesn't match", i) } } // Check collected metrics metrics := hybridDHT.GetHybridMetrics() if metrics.MockRequests != 10 { // 5 put + 5 get operations t.Errorf("Expected 10 mock requests, got %d", metrics.MockRequests) } if metrics.RealRequests != 0 { t.Errorf("Expected 0 real requests, got %d", metrics.RealRequests) } if metrics.TotalOperations != 10 { t.Errorf("Expected 10 total operations, got %d", metrics.TotalOperations) } // Verify metrics tracking if metrics.FallbackEvents != 0 { t.Errorf("Expected 0 fallback events, got %d", metrics.FallbackEvents) } if metrics.RecoveryEvents != 0 { t.Errorf("Expected 0 recovery events, got %d", metrics.RecoveryEvents) } // Verify latency tracking if metrics.MockLatency <= 0 { t.Error("Expected mock latency to be > 0") } // Verify error rate (should be 0 for successful operations) if metrics.MockErrorRate != 0.0 { t.Errorf("Expected 0 mock error rate, got %f", metrics.MockErrorRate) } t.Logf("✓ Metrics collection working correctly") t.Logf(" - Mock requests: %d", metrics.MockRequests) t.Logf(" - Total operations: %d", metrics.TotalOperations) t.Logf(" - Mock latency: %v", metrics.MockLatency) t.Logf(" - Mock error rate: %.2f%%", metrics.MockErrorRate*100.0) } func TestPhase2ConfigurationFromEnv(t *testing.T) { // Set environment variables os.Setenv("BZZZ_DHT_BACKEND", "mock") os.Setenv("BZZZ_FALLBACK_ON_ERROR", "true") os.Setenv("BZZZ_DHT_MAX_RETRIES", "5") os.Setenv("BZZZ_DHT_OPERATION_TIMEOUT", "15s") os.Setenv("BZZZ_MONITORING_ENABLED", "true") defer func() { // Clean up environment variables os.Unsetenv("BZZZ_DHT_BACKEND") os.Unsetenv("BZZZ_FALLBACK_ON_ERROR") os.Unsetenv("BZZZ_DHT_MAX_RETRIES") os.Unsetenv("BZZZ_DHT_OPERATION_TIMEOUT") os.Unsetenv("BZZZ_MONITORING_ENABLED") }() // Load configuration from environment config, err := config.LoadHybridConfig() if err != nil { t.Fatalf("Failed to load config from environment: %v", err) } // Verify configuration values if config.DHT.Backend != "mock" { t.Errorf("Expected backend 'mock', got '%s'", config.DHT.Backend) } if !config.DHT.FallbackOnError { t.Error("Expected fallback to be enabled") } if config.DHT.MaxRetries != 5 { t.Errorf("Expected 5 max retries, got %d", config.DHT.MaxRetries) } if config.DHT.OperationTimeout != 15*time.Second { t.Errorf("Expected 15s timeout, got %v", config.DHT.OperationTimeout) } if !config.Monitoring.Enabled { t.Error("Expected monitoring to be enabled") } // Test creating hybrid DHT with environment configuration logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT with env config: %v", err) } defer hybridDHT.Close() // Test basic operation ctx := context.Background() testKey := "env-config-test" testValue := []byte("environment-configuration") err = hybridDHT.PutValue(ctx, testKey, testValue) if err != nil { t.Fatalf("Failed to put value with env config: %v", err) } retrievedValue, err := hybridDHT.GetValue(ctx, testKey) if err != nil { t.Fatalf("Failed to get value with env config: %v", err) } if string(retrievedValue) != string(testValue) { t.Errorf("Retrieved value doesn't match with env config") } t.Logf("✓ Environment-based configuration working correctly") } func TestPhase2ConcurrentOperations(t *testing.T) { config := &config.HybridConfig{ DHT: config.HybridDHTConfig{ Backend: "mock", FallbackOnError: true, HealthCheckInterval: 5 * time.Second, }, } logger := &testLogger{} hybridDHT, err := dht.NewHybridDHT(config, logger) if err != nil { t.Fatalf("Failed to create hybrid DHT: %v", err) } defer hybridDHT.Close() ctx := context.Background() numWorkers := 10 numOperationsPerWorker := 5 // Channel to collect results results := make(chan error, numWorkers*numOperationsPerWorker*2) // *2 for put+get // Launch concurrent workers for i := 0; i < numWorkers; i++ { go func(workerId int) { for j := 0; j < numOperationsPerWorker; j++ { key := fmt.Sprintf("concurrent-worker-%d-op-%d", workerId, j) value := []byte(fmt.Sprintf("concurrent-value-%d-%d", workerId, j)) // Put operation err := hybridDHT.PutValue(ctx, key, value) results <- err // Get operation retrievedValue, err := hybridDHT.GetValue(ctx, key) if err == nil && string(retrievedValue) != string(value) { results <- fmt.Errorf("value mismatch for key %s", key) } else { results <- err } } }(i) } // Collect results totalOperations := numWorkers * numOperationsPerWorker * 2 errorCount := 0 for i := 0; i < totalOperations; i++ { if err := <-results; err != nil { t.Logf("Operation error: %v", err) errorCount++ } } if errorCount > 0 { t.Errorf("Expected no errors, but got %d errors out of %d operations", errorCount, totalOperations) } // Verify metrics metrics := hybridDHT.GetHybridMetrics() if metrics.TotalOperations != uint64(totalOperations) { t.Errorf("Expected %d total operations, got %d", totalOperations, metrics.TotalOperations) } if metrics.MockRequests != uint64(totalOperations) { t.Errorf("Expected %d mock requests, got %d", totalOperations, metrics.MockRequests) } if metrics.RealRequests != 0 { t.Errorf("Expected 0 real requests, got %d", metrics.RealRequests) } t.Logf("✓ Concurrent operations handled successfully") t.Logf(" - Total operations: %d", totalOperations) t.Logf(" - Error count: %d", errorCount) t.Logf(" - All operations used mock backend") } // testLogger implements the Logger interface for testing type testLogger struct{} func (l *testLogger) Info(msg string, fields ...interface{}) { fmt.Printf("[TEST-INFO] %s %v\n", msg, fields) } func (l *testLogger) Warn(msg string, fields ...interface{}) { fmt.Printf("[TEST-WARN] %s %v\n", msg, fields) } func (l *testLogger) Error(msg string, fields ...interface{}) { fmt.Printf("[TEST-ERROR] %s %v\n", msg, fields) } func (l *testLogger) Debug(msg string, fields ...interface{}) { fmt.Printf("[TEST-DEBUG] %s %v\n", msg, fields) }