Skip to content

Commit

Permalink
Add IsSuccess property and EnsureSuccessAsync method
Browse files Browse the repository at this point in the history
Introduce IsSuccess property to ApiResponse<T> and related interfaces to indicate request success. Add EnsureSuccessAsync method to handle errors, including deserialization issues. Refactor EnsureSuccessStatusCodeAsync to use ThrowsApiExceptionAsync. Update Dispose method for exception handling. Add unit tests to verify new behavior.
  • Loading branch information
marcominerva committed Oct 24, 2024
1 parent a7019f0 commit 39099c6
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 14 deletions.
16 changes: 16 additions & 0 deletions Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@ namespace Refit
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
public bool IsSuccess { get; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
public bool IsSuccessStatusCode { get; }
public string? ReasonPhrase { get; }
public System.Net.Http.HttpRequestMessage? RequestMessage { get; }
public Refit.RefitSettings Settings { get; }
public System.Net.HttpStatusCode StatusCode { get; }
public System.Version Version { get; }
public void Dispose() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessAsync() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessStatusCodeAsync() { }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)]
Expand Down Expand Up @@ -189,6 +195,11 @@ namespace Refit
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
bool IsSuccess { get; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
bool IsSuccessStatusCode { get; }
string? ReasonPhrase { get; }
System.Net.Http.HttpRequestMessage? RequestMessage { get; }
Expand All @@ -206,6 +217,11 @@ namespace Refit
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
new bool IsSuccess { get; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
new bool IsSuccessStatusCode { get; }
}
public interface IFormUrlEncodedParameterFormatter
Expand Down
16 changes: 16 additions & 0 deletions Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@ namespace Refit
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
public bool IsSuccess { get; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
public bool IsSuccessStatusCode { get; }
public string? ReasonPhrase { get; }
public System.Net.Http.HttpRequestMessage? RequestMessage { get; }
public Refit.RefitSettings Settings { get; }
public System.Net.HttpStatusCode StatusCode { get; }
public System.Version Version { get; }
public void Dispose() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessAsync() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessStatusCodeAsync() { }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)]
Expand Down Expand Up @@ -189,6 +195,11 @@ namespace Refit
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
bool IsSuccess { get; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
bool IsSuccessStatusCode { get; }
string? ReasonPhrase { get; }
System.Net.Http.HttpRequestMessage? RequestMessage { get; }
Expand All @@ -206,6 +217,11 @@ namespace Refit
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
new bool IsSuccess { get; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
new bool IsSuccessStatusCode { get; }
}
public interface IFormUrlEncodedParameterFormatter
Expand Down
45 changes: 45 additions & 0 deletions Refit.Tests/ResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,51 @@ public async Task When_BadRequest_EnsureSuccessStatusCodeAsync_ThrowsValidationE
Assert.Equal("type", actualException.Content.Type);
}

/// <summary>
/// Test to verify if IsSuccess returns false if we have a success status code, but there is a deserialization exception
/// </summary>
[Fact]
public async Task When_SerializationErrorOnSuccessStatusCode_IsSuccess_ShouldReturnFalse()
{
var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Invalid JSON")
};

mockHandler
.Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject")
.Respond(req => expectedResponse);

using var response = await fixture.GetApiResponseTestObject();

Assert.True(response.IsSuccessStatusCode);
Assert.False(response.IsSuccess);
}

/// <summary>
/// Test to verify if EnsureSuccessAsync throws an ApiException if we have a success status code, but there is a deserialization exception
/// </summary>
[Fact]
public async Task When_SerializationErrorOnSuccessStatusCode_EnsureSuccessAsync_ThrowsApiException()
{
var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Invalid JSON")
};

mockHandler
.Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject")
.Respond(req => expectedResponse);

using var response = await fixture.GetApiResponseTestObject();
var actualException = await Assert.ThrowsAsync<ApiException>(
() => response.EnsureSuccessAsync()
);

Assert.NotNull(actualException);
Assert.IsType<System.Text.Json.JsonException>(actualException.InnerException);
}

[Fact]
public async Task WhenProblemDetailsResponseContainsExtensions_ShouldHydrateExtensions()
{
Expand Down
73 changes: 59 additions & 14 deletions Refit/ApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,21 @@ public sealed class ApiResponse<T>(
/// Indicates whether the request was successful.
/// </summary>
#if NET6_0_OR_GREATER
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
#endif
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;

/// <summary>
/// Indicates whether the request was successful and there wasn't any other error (for example, during deserialization).
/// </summary>
#if NET6_0_OR_GREATER
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
#endif
public bool IsSuccess => IsSuccessStatusCode && Error is null;

/// <summary>
/// The reason phrase which typically is sent by the server together with the status code.
/// </summary>
Expand Down Expand Up @@ -119,20 +128,22 @@ public async Task<ApiResponse<T>> EnsureSuccessStatusCodeAsync()
{
if (!IsSuccessStatusCode)
{
var exception =
Error
?? await ApiException
.Create(
response.RequestMessage!,
response.RequestMessage!.Method,
response,
Settings
)
.ConfigureAwait(false);
await ThrowsApiExceptionAsync().ConfigureAwait(false);
}

Dispose();
return this;
}

Check warning on line 135 in Refit/ApiResponse.cs

View check run for this annotation

Codecov / codecov/patch

Refit/ApiResponse.cs#L134-L135

Added lines #L134 - L135 were not covered by tests

throw exception;
/// <summary>
/// Ensures the request was successful and without any other error by throwing an exception in case of failure
/// </summary>
/// <returns>The current <see cref="ApiResponse{T}"/></returns>
/// <exception cref="ApiException"></exception>
public async Task<ApiResponse<T>> EnsureSuccessAsync()
{
if (!IsSuccess)
{
await ThrowsApiExceptionAsync().ConfigureAwait(false);
}

return this;
Expand All @@ -147,6 +158,24 @@ void Dispose(bool disposing)

response.Dispose();
}

private async Task<ApiException> ThrowsApiExceptionAsync()
{
var exception =
Error
?? await ApiException
.Create(
response.RequestMessage!,
response.RequestMessage!.Method,
response,
Settings
)
.ConfigureAwait(false);

Dispose();

throw exception;
}
}

/// <inheritdoc/>
Expand All @@ -171,10 +200,17 @@ public interface IApiResponse<out T> : IApiResponse
/// <summary>
/// Indicates whether the request was successful.
/// </summary>
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
new bool IsSuccessStatusCode { get; }

/// <summary>
/// Indicates whether the request was successful and there wasn't any other error (for example, during deserialization).
/// </summary>
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
new bool IsSuccess { get; }
#endif

/// <summary>
Expand Down Expand Up @@ -207,6 +243,15 @@ public interface IApiResponse : IDisposable
#endif
bool IsSuccessStatusCode { get; }

/// <summary>
/// Indicates whether the request was successful and there wasn't any other error (for example, during deserialization).
/// </summary>
#if NET6_0_OR_GREATER
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
#endif
bool IsSuccess { get; }

/// <summary>
/// HTTP response status code.
/// </summary>
Expand Down

0 comments on commit 39099c6

Please sign in to comment.