using DeepL; using FictionArchive.Service.TranslationService.Services.Resilience; using FluentAssertions; using Polly; using Xunit; namespace FictionArchive.Service.TranslationService.Tests.Resilience; public class TranslationResiliencePipelineTests { [Fact] public async Task RetriesTransientHttpRequestException_ThenSucceeds() { var pipeline = TranslationResiliencePipeline.Build(); var attempts = 0; var result = await pipeline.ExecuteAsync(async _ => { attempts++; if (attempts < 3) { throw new HttpRequestException("transient"); } return await ValueTask.FromResult("ok"); }); attempts.Should().Be(3); result.Should().Be("ok"); } [Fact] public async Task RetriesDeepLConnectionException() { var pipeline = TranslationResiliencePipeline.Build(); var attempts = 0; var result = await pipeline.ExecuteAsync(async _ => { attempts++; if (attempts < 2) { throw new ConnectionException("transient", new HttpRequestException("inner")); } return await ValueTask.FromResult("ok"); }); attempts.Should().Be(2); result.Should().Be("ok"); } [Fact] public async Task RetriesDeepLTooManyRequestsException() { var pipeline = TranslationResiliencePipeline.Build(); var attempts = 0; var result = await pipeline.ExecuteAsync(async _ => { attempts++; if (attempts < 2) { throw new TooManyRequestsException("rate limited"); } return await ValueTask.FromResult("ok"); }); attempts.Should().Be(2); result.Should().Be("ok"); } [Fact] public async Task DoesNotRetryNonTransientException() { var pipeline = TranslationResiliencePipeline.Build(); var attempts = 0; var act = async () => await pipeline.ExecuteAsync(async _ => { attempts++; throw new ArgumentException("permanent"); #pragma warning disable CS0162 return await ValueTask.FromResult("never"); #pragma warning restore CS0162 }); await act.Should().ThrowAsync(); attempts.Should().Be(1); } [Fact] public async Task DoesNotRetryDeepLAuthorizationException() { var pipeline = TranslationResiliencePipeline.Build(); var attempts = 0; var act = async () => await pipeline.ExecuteAsync(async _ => { attempts++; throw new AuthorizationException("bad key"); #pragma warning disable CS0162 return await ValueTask.FromResult("never"); #pragma warning restore CS0162 }); await act.Should().ThrowAsync(); attempts.Should().Be(1); } [Fact] public async Task ExhaustsRetryBudget_ThenRethrows() { var pipeline = TranslationResiliencePipeline.Build(); var attempts = 0; var act = async () => await pipeline.ExecuteAsync(async _ => { attempts++; throw new HttpRequestException("always transient"); #pragma warning disable CS0162 return await ValueTask.FromResult("never"); #pragma warning restore CS0162 }); await act.Should().ThrowAsync(); attempts.Should().Be(4); // initial attempt + 3 retries } }