Files
satellite-provider/SatelliteProvider.Tests/RegionFailureClassifierTests.cs
T
Oleksandr Bezdieniezhnykh 5a28f67d33
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[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>
2026-05-11 00:07:31 +03:00

169 lines
5.5 KiB
C#

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>();
}
}