using Xunit; using Svrnty.MCP.Gateway.Infrastructure.Health; using Svrnty.MCP.Gateway.Core.Models; namespace Svrnty.MCP.Gateway.Infrastructure.Tests.Health; /// /// Unit tests for CircuitBreaker following TDD approach. /// Tests circuit breaker states: Closed, Open, HalfOpen. /// public class CircuitBreakerTests { [Fact] public async Task ExecuteAsync_InClosedState_AllowsExecution() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 3 }; var executed = false; // Act await breaker.ExecuteAsync(async () => { executed = true; await Task.CompletedTask; }); // Assert Assert.True(executed); Assert.Equal(CircuitBreakerState.Closed, breaker.State); } [Fact] public async Task ExecuteAsync_AfterMultipleFailures_OpensCircuit() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 3, OpenTimeout = TimeSpan.FromSeconds(1) }; // Act - trigger 3 failures for (int i = 0; i < 3; i++) { try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Simulated failure"); }); } catch { // Expected } } // Assert Assert.Equal(CircuitBreakerState.Open, breaker.State); } [Fact] public async Task ExecuteAsync_WhenOpen_ThrowsCircuitBreakerOpenException() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 1, OpenTimeout = TimeSpan.FromSeconds(10) }; // Trip the breaker try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Failure"); }); } catch { // Expected } // Act & Assert - breaker should be open await Assert.ThrowsAsync(() => breaker.ExecuteAsync(async () => await Task.CompletedTask)); } [Fact] public async Task ExecuteAsync_AfterTimeout_TransitionsToHalfOpen() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 1, OpenTimeout = TimeSpan.FromMilliseconds(100), SuccessThreshold = 1 // Close after 1 success }; // Trip the breaker try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Failure"); }); } catch { // Expected } Assert.Equal(CircuitBreakerState.Open, breaker.State); // Act - wait for timeout await Task.Delay(150); // Try another execution (should transition to half-open) var executed = false; await breaker.ExecuteAsync(async () => { executed = true; await Task.CompletedTask; }); // Assert Assert.True(executed); Assert.Equal(CircuitBreakerState.Closed, breaker.State); // Successful execution closes the circuit } [Fact] public async Task ExecuteAsync_InHalfOpen_SuccessfulCallClosesCircuit() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 1, OpenTimeout = TimeSpan.FromMilliseconds(100), SuccessThreshold = 1 }; // Trip the breaker try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Failure"); }); } catch { // Expected } await Task.Delay(150); // Wait for open timeout // Act - successful call in half-open await breaker.ExecuteAsync(async () => await Task.CompletedTask); // Assert Assert.Equal(CircuitBreakerState.Closed, breaker.State); } [Fact] public async Task ExecuteAsync_InHalfOpen_FailureReopensCircuit() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 1, OpenTimeout = TimeSpan.FromMilliseconds(100) }; // Trip the breaker try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Failure"); }); } catch { // Expected } await Task.Delay(150); // Wait for open timeout (now in half-open) // Act - fail in half-open try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Another failure"); }); } catch { // Expected } // Assert Assert.Equal(CircuitBreakerState.Open, breaker.State); } [Fact] public async Task ExecuteAsync_WithReturnValue_ReturnsResult() { // Arrange var breaker = new CircuitBreaker(); var expectedValue = 42; // Act var result = await breaker.ExecuteAsync(async () => { await Task.CompletedTask; return expectedValue; }); // Assert Assert.Equal(expectedValue, result); } [Fact] public void Reset_ClosesCircuitAndClearsFailures() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 1 }; try { breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Failure"); }).GetAwaiter().GetResult(); } catch { // Expected } Assert.Equal(CircuitBreakerState.Open, breaker.State); // Act breaker.Reset(); // Assert Assert.Equal(CircuitBreakerState.Closed, breaker.State); } [Fact] public void Constructor_SetsDefaultValues() { // Act var breaker = new CircuitBreaker(); // Assert Assert.Equal(CircuitBreakerState.Closed, breaker.State); Assert.Equal(5, breaker.FailureThreshold); Assert.Equal(3, breaker.SuccessThreshold); Assert.Equal(TimeSpan.FromSeconds(60), breaker.OpenTimeout); } [Fact] public async Task ExecuteAsync_TracksConcurrentFailures() { // Arrange var breaker = new CircuitBreaker { FailureThreshold = 5 }; var failureCount = 0; // Act - cause 4 failures (not enough to trip) for (int i = 0; i < 4; i++) { try { await breaker.ExecuteAsync(async () => { await Task.CompletedTask; throw new Exception("Failure"); }); } catch { failureCount++; } } // Assert - should still be closed Assert.Equal(CircuitBreakerState.Closed, breaker.State); Assert.Equal(4, failureCount); } }