mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 02:51:14 +00:00
[AZ-359] Refactor C07: collapse RegionService catch ladder via RegionFailureClassifier
Replace 9 nearly-identical catch blocks in RegionService.ProcessRegionAsync with a single catch (Exception ex) that delegates to RegionFailureClassifier.Classify, returning a typed (category, errorMessage) pair. Preserves all original error messages stored in region summary files; failure-path call into HandleProcessingFailureAsync is unchanged. Net source delta: -38 lines in RegionService, +71 lines in new RegionFailureClassifier (pure static), +10 unit tests covering each category, precedence, status-code propagation, null guard. Tests: 68 unit (was 58) + 5 smoke + 3 stub-contract integration tests pass. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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<ArgumentNullException>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user