Files
Omika/tests/Omika.Tests/Observability/External/ExternalCircuitBreakerTests.cs
T
2026-10-23 18:46:50 +02:00

109 lines
3.4 KiB
C#

using Omika.Core.Observability.External;
namespace Omika.Tests.Observability.External;
/// <summary>Disjoncteur du pont externe (§16) : déterministe (horloge injectée).</summary>
public class ExternalCircuitBreakerTests
{
[Fact]
public void StartsHealthy()
{
var circuit = NewCircuit();
Assert.Equal(ExternalObservabilityState.Healthy, circuit.State);
Assert.True(circuit.AllowExport());
}
[Fact]
public void OpensAfterThresholdFailures()
{
var circuit = NewCircuit(); // seuil = 3
circuit.RecordFailure();
circuit.RecordFailure();
Assert.Equal(ExternalObservabilityState.Degraded, circuit.State);
Assert.True(circuit.AllowExport()); // dégradé mais autorisé
circuit.RecordFailure();
Assert.Equal(ExternalObservabilityState.Open, circuit.State);
}
[Fact]
public void DropsWhileOpen()
{
var clock = new MutableClock();
var circuit = new ExternalCircuitBreaker(2, TimeSpan.FromSeconds(30), clock.Now);
circuit.RecordFailure();
circuit.RecordFailure();
Assert.False(circuit.AllowExport()); // open : drop
Assert.False(circuit.AllowExport());
}
[Fact]
public void ProbesAfterCooldown()
{
var clock = new MutableClock();
var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now);
circuit.RecordFailure();
Assert.False(circuit.AllowExport());
clock.Advance(TimeSpan.FromSeconds(31));
Assert.True(circuit.AllowExport()); // half-open : autorise UNE sonde
Assert.Equal(ExternalObservabilityState.Degraded, circuit.State);
}
[Fact]
public void SuccessClosesCircuit()
{
var clock = new MutableClock();
var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now);
circuit.RecordFailure();
clock.Advance(TimeSpan.FromSeconds(31));
Assert.True(circuit.AllowExport());
circuit.RecordSuccess();
Assert.Equal(ExternalObservabilityState.Healthy, circuit.State);
Assert.True(circuit.AllowExport());
}
[Fact]
public void ProbeFailureReopensCircuit()
{
var clock = new MutableClock();
var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now);
circuit.RecordFailure();
clock.Advance(TimeSpan.FromSeconds(31));
Assert.True(circuit.AllowExport()); // half-open sonde
circuit.RecordFailure(); // sonde échoue → ré-ouverture
Assert.Equal(ExternalObservabilityState.Open, circuit.State);
Assert.False(circuit.AllowExport());
}
[Fact]
public void NeverAllowsNegativeOrZeroCooldown()
{
var clock = new MutableClock();
var circuit = new ExternalCircuitBreaker(1, TimeSpan.Zero, clock.Now);
circuit.RecordFailure();
Assert.False(circuit.AllowExport()); // cooldown ramené à >= 1 s : encore ouvert
clock.Advance(TimeSpan.FromSeconds(2));
Assert.True(circuit.AllowExport()); // sonde possible après cooldown positif
}
private static ExternalCircuitBreaker NewCircuit() =>
new(3, TimeSpan.FromSeconds(30));
private sealed class MutableClock
{
private DateTimeOffset _now = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
public DateTimeOffset Now() => _now;
public void Advance(TimeSpan delta) => _now += delta;
}
}