package arbitrum import ( "context" "os" "testing" "time" "github.com/fraktal/mev-beta/internal/config" "github.com/stretchr/testify/assert" ) func TestConnectionManager_GetPrimaryEndpoint(t *testing.T) { tests := []struct { name string envValue string configValue string expectedResult string }{ { name: "Environment variable takes precedence", envValue: "wss://env-endpoint.com", configValue: "wss://config-endpoint.com", expectedResult: "wss://env-endpoint.com", }, { name: "Config value used when env not set", envValue: "", configValue: "wss://config-endpoint.com", expectedResult: "wss://config-endpoint.com", }, { name: "Default fallback when neither set", envValue: "", configValue: "", expectedResult: "wss://arbitrum-mainnet.core.chainstack.com/f69d14406bc00700da9b936504e1a870", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set up environment if tt.envValue != "" { os.Setenv("ARBITRUM_RPC_ENDPOINT", tt.envValue) } else { os.Unsetenv("ARBITRUM_RPC_ENDPOINT") } defer os.Unsetenv("ARBITRUM_RPC_ENDPOINT") // Create connection manager with config cfg := &config.ArbitrumConfig{ RPCEndpoint: tt.configValue, } cm := NewConnectionManager(cfg) // Test result := cm.getPrimaryEndpoint() assert.Equal(t, tt.expectedResult, result) }) } } func TestConnectionManager_GetFallbackEndpoints(t *testing.T) { tests := []struct { name string envValue string configEndpoints []config.EndpointConfig expectedContains []string }{ { name: "Environment variable endpoints", envValue: "https://endpoint1.com,https://endpoint2.com, https://endpoint3.com ", expectedContains: []string{ "https://endpoint1.com", "https://endpoint2.com", "https://endpoint3.com", }, }, { name: "Config endpoints used when env not set", envValue: "", configEndpoints: []config.EndpointConfig{ {URL: "https://config1.com"}, {URL: "https://config2.com"}, }, expectedContains: []string{ "https://config1.com", "https://config2.com", }, }, { name: "Default endpoints when nothing configured", envValue: "", expectedContains: []string{ "https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com", "https://arbitrum-one.publicnode.com", "https://arbitrum-one.public.blastapi.io", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set up environment if tt.envValue != "" { os.Setenv("ARBITRUM_FALLBACK_ENDPOINTS", tt.envValue) } else { os.Unsetenv("ARBITRUM_FALLBACK_ENDPOINTS") } defer os.Unsetenv("ARBITRUM_FALLBACK_ENDPOINTS") // Create connection manager with config cfg := &config.ArbitrumConfig{ FallbackEndpoints: tt.configEndpoints, } cm := NewConnectionManager(cfg) // Test result := cm.getFallbackEndpoints() // Check that all expected endpoints are present for _, expected := range tt.expectedContains { assert.Contains(t, result, expected, "Expected endpoint %s not found in results", expected) } }) } } func TestConnectionManager_ConnectWithTimeout(t *testing.T) { cm := NewConnectionManager(&config.ArbitrumConfig{}) ctx := context.Background() t.Run("Invalid endpoint fails quickly", func(t *testing.T) { start := time.Now() _, err := cm.connectWithTimeout(ctx, "wss://invalid-endpoint-that-does-not-exist.com") duration := time.Since(start) assert.Error(t, err) assert.Less(t, duration, 15*time.Second, "Should fail within timeout period") }) t.Run("Connection respects context cancellation", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() start := time.Now() _, err := cm.connectWithTimeout(ctx, "wss://very-slow-endpoint.com") duration := time.Since(start) assert.Error(t, err) assert.Less(t, duration, 2*time.Second, "Should respect context timeout") }) } func TestConnectionManager_GetClientWithRetry(t *testing.T) { cm := NewConnectionManager(&config.ArbitrumConfig{}) ctx := context.Background() t.Run("Retry logic with exponential backoff", func(t *testing.T) { start := time.Now() // This should fail but test the retry mechanism _, err := cm.GetClientWithRetry(ctx, 3) duration := time.Since(start) // Should have attempted 3 times with exponential backoff // First attempt: immediate // Second attempt: 1 second wait // Third attempt: 2 second wait // Total minimum time should be around 3 seconds assert.Error(t, err) assert.GreaterOrEqual(t, duration, 3*time.Second, "Should include retry delays") assert.Contains(t, err.Error(), "failed to connect after 3 attempts") }) } func TestConnectionManager_HealthyClient(t *testing.T) { t.Run("GetHealthyClient returns error when no endpoints work", func(t *testing.T) { // Set invalid endpoints to test failure case os.Setenv("ARBITRUM_RPC_ENDPOINT", "wss://invalid-endpoint.com") os.Setenv("ARBITRUM_FALLBACK_ENDPOINTS", "https://invalid1.com,https://invalid2.com") defer func() { os.Unsetenv("ARBITRUM_RPC_ENDPOINT") os.Unsetenv("ARBITRUM_FALLBACK_ENDPOINTS") }() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := GetHealthyClient(ctx) assert.Error(t, err, "Should fail when all endpoints are invalid") }) } func TestConnectionManager_Configuration(t *testing.T) { t.Run("Environment variables override config file", func(t *testing.T) { // Set environment variables os.Setenv("ARBITRUM_RPC_ENDPOINT", "wss://env-primary.com") os.Setenv("ARBITRUM_FALLBACK_ENDPOINTS", "https://env-fallback1.com,https://env-fallback2.com") defer func() { os.Unsetenv("ARBITRUM_RPC_ENDPOINT") os.Unsetenv("ARBITRUM_FALLBACK_ENDPOINTS") }() // Create config with different values cfg := &config.ArbitrumConfig{ RPCEndpoint: "wss://config-primary.com", FallbackEndpoints: []config.EndpointConfig{ {URL: "https://config-fallback1.com"}, {URL: "https://config-fallback2.com"}, }, } cm := NewConnectionManager(cfg) // Test that environment variables take precedence primary := cm.getPrimaryEndpoint() assert.Equal(t, "wss://env-primary.com", primary) fallbacks := cm.getFallbackEndpoints() assert.Contains(t, fallbacks, "https://env-fallback1.com") assert.Contains(t, fallbacks, "https://env-fallback2.com") assert.NotContains(t, fallbacks, "https://config-fallback1.com") assert.NotContains(t, fallbacks, "https://config-fallback2.com") }) } func TestConnectionManager_Lifecycle(t *testing.T) { cm := NewConnectionManager(&config.ArbitrumConfig{}) t.Run("Close handles nil clients gracefully", func(t *testing.T) { // Should not panic assert.NotPanics(t, func() { cm.Close() }) }) t.Run("Multiple close calls are safe", func(t *testing.T) { assert.NotPanics(t, func() { cm.Close() cm.Close() cm.Close() }) }) } // Integration test that requires real network access func TestConnectionManager_RealEndpoints(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } t.Run("Public Arbitrum RPC endpoints should be reachable", func(t *testing.T) { publicEndpoints := []string{ "https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com", "https://arbitrum-one.publicnode.com", } cm := NewConnectionManager(&config.ArbitrumConfig{}) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() successCount := 0 for _, endpoint := range publicEndpoints { client, err := cm.connectWithTimeout(ctx, endpoint) if err == nil { client.Close() successCount++ t.Logf("Successfully connected to %s", endpoint) } else { t.Logf("Failed to connect to %s: %v", endpoint, err) } } // At least one public endpoint should be working assert.Greater(t, successCount, 0, "At least one public Arbitrum RPC should be reachable") }) } // Benchmark connection establishment func BenchmarkConnectionManager_GetClient(b *testing.B) { cm := NewConnectionManager(&config.ArbitrumConfig{}) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { // This will fail but tests the connection attempt performance _, _ = cm.GetClient(ctx) } }