using ZB.MOM.WW.CBDDC.Core.Exceptions; using ZB.MOM.WW.CBDDC.Core.Network; using ZB.MOM.WW.CBDDC.Core.Resilience; namespace ZB.MOM.WW.CBDDC.Core.Tests; public class RetryPolicyTests { /// /// Verifies transient failures are retried until a successful result is returned. /// [Fact] public async Task ExecuteAsync_WhenTransientFailureEventuallySucceeds_RetriesAndReturnsResult() { var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 3, retryDelayMs: 1)); var attempts = 0; int result = await policy.ExecuteAsync(async () => { attempts++; if (attempts < 3) throw new NetworkException("transient"); await Task.CompletedTask; return 42; }, "test-op"); result.ShouldBe(42); attempts.ShouldBe(3); } /// /// Verifies transient failures throw retry exhausted when all retries are consumed. /// [Fact] public async Task ExecuteAsync_WhenTransientFailureExhausted_ThrowsRetryExhaustedException() { var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 2, retryDelayMs: 1)); var attempts = 0; var ex = await Should.ThrowAsync(() => policy.ExecuteAsync(() => { attempts++; throw new NetworkException("still transient"); }, "test-op")); ex.ErrorCode.ShouldBe("RETRY_EXHAUSTED"); ex.InnerException.ShouldBeOfType(); attempts.ShouldBe(2); } /// /// Verifies non-transient failures are not retried. /// [Fact] public async Task ExecuteAsync_WhenFailureIsNonTransient_DoesNotRetry() { var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 3, retryDelayMs: 1)); var attempts = 0; await Should.ThrowAsync(() => policy.ExecuteAsync(() => { attempts++; throw new InvalidOperationException("non-transient"); }, "test-op")); attempts.ShouldBe(1); } private static IPeerNodeConfigurationProvider CreateConfigProvider(int retryAttempts, int retryDelayMs) { var configProvider = Substitute.For(); configProvider.GetConfiguration().Returns(new PeerNodeConfiguration { RetryAttempts = retryAttempts, RetryDelayMs = retryDelayMs }); return configProvider; } }