using System.Net; using FluentAssertions; using SatelliteProvider.Common.Exceptions; using SatelliteProvider.Services.RegionProcessing; namespace SatelliteProvider.Tests; public class RegionFailureClassifierTests { [Fact] public void Classify_TaskCanceled_WhenTimeoutFired_ReturnsTimeout() { // Arrange var timeoutCts = new CancellationTokenSource(); timeoutCts.Cancel(); var external = new CancellationToken(); var ex = new TaskCanceledException("timed out"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, external); // Assert result.Category.Should().Be(RegionFailureCategory.Timeout); result.ErrorMessage.Should().Be(RegionFailureClassifier.TimeoutMessage); } [Fact] public void Classify_TaskCanceled_WhenExternalTokenCancelled_ReturnsExternalCancellation() { // Arrange var timeoutCts = new CancellationTokenSource(); var externalCts = new CancellationTokenSource(); externalCts.Cancel(); var ex = new TaskCanceledException("shutdown"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, externalCts.Token); // Assert result.Category.Should().Be(RegionFailureCategory.ExternalCancellation); result.ErrorMessage.Should().Be(RegionFailureClassifier.ExternalCancellationMessage); } [Fact] public void Classify_TaskCanceled_WithNoTokenCancelled_ReturnsTaskCanceledOther() { // Arrange var timeoutCts = new CancellationTokenSource(); var ex = new TaskCanceledException("inner http timeout"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, CancellationToken.None); // Assert result.Category.Should().Be(RegionFailureCategory.TaskCanceledOther); result.ErrorMessage.Should().Contain("inner http timeout"); result.ErrorMessage.Should().Contain("HttpClient timeout"); } [Fact] public void Classify_OperationCanceled_WhenTimeoutFired_ReturnsTimeout() { // Arrange var timeoutCts = new CancellationTokenSource(); timeoutCts.Cancel(); var ex = new OperationCanceledException("op cancelled"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, CancellationToken.None); // Assert result.Category.Should().Be(RegionFailureCategory.Timeout); result.ErrorMessage.Should().Be(RegionFailureClassifier.TimeoutMessage); } [Fact] public void Classify_OperationCanceled_WithoutTimeout_ReturnsOperationCanceledOther() { // Arrange var timeoutCts = new CancellationTokenSource(); var ex = new OperationCanceledException("manual abort"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, CancellationToken.None); // Assert result.Category.Should().Be(RegionFailureCategory.OperationCanceledOther); result.ErrorMessage.Should().Contain("manual abort"); } [Fact] public void Classify_RateLimitException_ReturnsRateLimit() { // Arrange var timeoutCts = new CancellationTokenSource(); var ex = new RateLimitException("retries exhausted"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, CancellationToken.None); // Assert result.Category.Should().Be(RegionFailureCategory.RateLimit); result.ErrorMessage.Should().Contain("retries exhausted"); result.ErrorMessage.Should().Contain("Rate limit exceeded"); } [Fact] public void Classify_HttpRequestException_ReturnsNetwork_WithStatusCode() { // Arrange var timeoutCts = new CancellationTokenSource(); var ex = new HttpRequestException("server down", inner: null, statusCode: HttpStatusCode.BadGateway); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, CancellationToken.None); // Assert result.Category.Should().Be(RegionFailureCategory.Network); result.ErrorMessage.Should().Contain("BadGateway"); result.ErrorMessage.Should().Contain("server down"); } [Fact] public void Classify_UnknownException_ReturnsUnexpected() { // Arrange var timeoutCts = new CancellationTokenSource(); var ex = new InvalidOperationException("boom"); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, CancellationToken.None); // Assert result.Category.Should().Be(RegionFailureCategory.Unexpected); result.ErrorMessage.Should().Contain("InvalidOperationException"); result.ErrorMessage.Should().Contain("boom"); } [Fact] public void Classify_TimeoutTakesPrecedenceOverExternalCancellation() { // Arrange var timeoutCts = new CancellationTokenSource(); timeoutCts.Cancel(); var externalCts = new CancellationTokenSource(); externalCts.Cancel(); var ex = new TaskCanceledException(); // Act var result = RegionFailureClassifier.Classify(ex, timeoutCts, externalCts.Token); // Assert result.Category.Should().Be(RegionFailureCategory.Timeout); } [Fact] public void Classify_NullException_Throws() { // Arrange var timeoutCts = new CancellationTokenSource(); // Act Action act = () => RegionFailureClassifier.Classify(null!, timeoutCts, CancellationToken.None); // Assert act.Should().Throw(); } }