299099f716
Added 11 new tests covering: - POST method tests (200 success, 401 unauthorized, no-body variant) - Bytes tests (GET 200, GET 404, POST 200) - Non-GET status code tests (POST 400 validation, bytes 500, multipart 401/200) All 23 ApiClientBaseTests now pass.
388 lines
11 KiB
C#
388 lines
11 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using RichardSzalay.MockHttp;
|
|
using Shouldly;
|
|
|
|
namespace JdeScoping.Client.Tests.Services;
|
|
|
|
public class ApiClientBaseTests
|
|
{
|
|
private readonly MockHttpMessageHandler _mockHttp;
|
|
private readonly TestableApiClient _client;
|
|
|
|
public ApiClientBaseTests()
|
|
{
|
|
_mockHttp = new MockHttpMessageHandler();
|
|
var httpClient = new HttpClient(_mockHttp) { BaseAddress = new Uri("http://localhost/") };
|
|
_client = new TestableApiClient(httpClient);
|
|
}
|
|
|
|
public record TestDto(int Id, string Name);
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns200_MapsToSuccessValue()
|
|
{
|
|
// Arrange
|
|
var expected = new TestDto(42, "Test");
|
|
_mockHttp.When("/api/test")
|
|
.Respond("application/json", JsonSerializer.Serialize(expected));
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
result.Value.Id.ShouldBe(42);
|
|
result.Value.Name.ShouldBe("Test");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns404_MapsToNotFound()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.NotFound);
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsNotFound.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns400_WithValidationErrors_MapsToValidationError()
|
|
{
|
|
// Arrange - use actual ValidationProblemDetails structure to match production
|
|
var validationProblem = new Microsoft.AspNetCore.Mvc.ValidationProblemDetails
|
|
{
|
|
Errors =
|
|
{
|
|
["Name"] = new[] { "Name is required" },
|
|
["Id"] = new[] { "Id must be positive" }
|
|
}
|
|
};
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.BadRequest, "application/json", JsonSerializer.Serialize(validationProblem, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsValidationError.ShouldBeTrue();
|
|
result.ValidationError.FieldErrors.ShouldContainKey("Name");
|
|
result.ValidationError.FieldErrors["Name"].ShouldContain("Name is required");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns401_MapsToUnauthorized()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.Unauthorized);
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsUnauthorized.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns403_MapsToForbidden()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.Forbidden);
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsForbidden.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns500_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.InternalServerError, "text/plain", "Internal Server Error");
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
result.Error.StatusCode.ShouldBe(500);
|
|
}
|
|
|
|
// Edge cases
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns200_EmptyBody_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.OK, "application/json", "");
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns200_InvalidJson_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.OK, "application/json", "not valid json {{{");
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns204_ForUnitType_MapsToSuccess()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.NoContent);
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<Unit>("/api/test");
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns204_ForNonUnitType_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.NoContent);
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_NetworkException_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Throw(new HttpRequestException("Network failure"));
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
result.Error.Message.ShouldContain("Network failure");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Timeout_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/test")
|
|
.Throw(new TaskCanceledException("Request timeout"));
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAsync_Returns400_WithoutValidationFormat_MapsToApiError()
|
|
{
|
|
// Arrange - Bad request without standard validation problem format
|
|
_mockHttp.When("/api/test")
|
|
.Respond(HttpStatusCode.BadRequest, "text/plain", "Bad request");
|
|
|
|
// Act
|
|
var result = await _client.GetAsync<TestDto>("/api/test");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
result.Error.StatusCode.ShouldBe(400);
|
|
}
|
|
|
|
// POST tests
|
|
|
|
[Fact]
|
|
public async Task PostAsync_Returns200_MapsToSuccessValue()
|
|
{
|
|
// Arrange
|
|
var expected = new TestDto(99, "Created");
|
|
_mockHttp.When(HttpMethod.Post, "/api/test")
|
|
.Respond("application/json", JsonSerializer.Serialize(expected));
|
|
|
|
// Act
|
|
var result = await _client.PostAsync<TestDto, TestDto>("/api/test", new TestDto(0, "Input"));
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
result.Value.Id.ShouldBe(99);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAsync_Returns401_MapsToUnauthorized()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When(HttpMethod.Post, "/api/test")
|
|
.Respond(HttpStatusCode.Unauthorized);
|
|
|
|
// Act
|
|
var result = await _client.PostAsync<TestDto, TestDto>("/api/test", new TestDto(0, "Input"));
|
|
|
|
// Assert
|
|
result.IsUnauthorized.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostAsync_WithNoBody_Returns200()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When(HttpMethod.Post, "/api/test")
|
|
.Respond("application/json", JsonSerializer.Serialize(new Unit()));
|
|
|
|
// Act
|
|
var result = await _client.PostAsync<Unit>("/api/test");
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
}
|
|
|
|
// Bytes tests
|
|
|
|
[Fact]
|
|
public async Task GetBytesAsync_Returns200_MapsToSuccessBytes()
|
|
{
|
|
// Arrange
|
|
var expectedBytes = new byte[] { 0x50, 0x4B, 0x03, 0x04 }; // ZIP header
|
|
_mockHttp.When("/api/download")
|
|
.Respond("application/octet-stream", new MemoryStream(expectedBytes));
|
|
|
|
// Act
|
|
var result = await _client.GetBytesAsync("/api/download");
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
result.Value.ShouldBe(expectedBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBytesAsync_Returns404_MapsToNotFound()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/download")
|
|
.Respond(HttpStatusCode.NotFound);
|
|
|
|
// Act
|
|
var result = await _client.GetBytesAsync("/api/download");
|
|
|
|
// Assert
|
|
result.IsNotFound.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostForBytesAsync_Returns200_MapsToSuccessBytes()
|
|
{
|
|
// Arrange
|
|
var expectedBytes = new byte[] { 0x50, 0x4B, 0x03, 0x04 };
|
|
_mockHttp.When(HttpMethod.Post, "/api/download")
|
|
.Respond("application/octet-stream", new MemoryStream(expectedBytes));
|
|
|
|
// Act
|
|
var result = await _client.PostForBytesAsync("/api/download", new { filter = "test" });
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
result.Value.ShouldBe(expectedBytes);
|
|
}
|
|
|
|
// Non-GET status code tests
|
|
|
|
[Fact]
|
|
public async Task PostAsync_Returns400_WithValidationErrors_MapsToValidationError()
|
|
{
|
|
// Arrange
|
|
var validationProblem = new Microsoft.AspNetCore.Mvc.ValidationProblemDetails
|
|
{
|
|
Errors = { ["Field"] = new[] { "Required" } }
|
|
};
|
|
_mockHttp.When(HttpMethod.Post, "/api/test")
|
|
.Respond(HttpStatusCode.BadRequest, "application/json",
|
|
JsonSerializer.Serialize(validationProblem, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
|
|
|
|
// Act
|
|
var result = await _client.PostAsync<TestDto, TestDto>("/api/test", new TestDto(0, "Input"));
|
|
|
|
// Assert
|
|
result.IsValidationError.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetBytesAsync_Returns500_MapsToApiError()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When("/api/download")
|
|
.Respond(HttpStatusCode.InternalServerError, "text/plain", "Server error");
|
|
|
|
// Act
|
|
var result = await _client.GetBytesAsync("/api/download");
|
|
|
|
// Assert
|
|
result.IsError.ShouldBeTrue();
|
|
result.Error.StatusCode.ShouldBe(500);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMultipartAsync_Returns401_MapsToUnauthorized()
|
|
{
|
|
// Arrange
|
|
_mockHttp.When(HttpMethod.Post, "/api/upload")
|
|
.Respond(HttpStatusCode.Unauthorized);
|
|
|
|
using var stream = new MemoryStream(new byte[] { 1, 2, 3 });
|
|
|
|
// Act
|
|
var result = await _client.PostMultipartAsync<TestDto>("/api/upload", stream, "test.xlsx");
|
|
|
|
// Assert
|
|
result.IsUnauthorized.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostMultipartAsync_Returns200_MapsToSuccessValue()
|
|
{
|
|
// Arrange
|
|
var expected = new TestDto(1, "Uploaded");
|
|
_mockHttp.When(HttpMethod.Post, "/api/upload")
|
|
.With(req => req.Content?.Headers.ContentType?.MediaType == "multipart/form-data")
|
|
.Respond("application/json", JsonSerializer.Serialize(expected));
|
|
|
|
using var stream = new MemoryStream(new byte[] { 1, 2, 3 });
|
|
|
|
// Act
|
|
var result = await _client.PostMultipartAsync<TestDto>("/api/upload", stream, "test.xlsx");
|
|
|
|
// Assert
|
|
result.IsSuccess.ShouldBeTrue();
|
|
result.Value.Id.ShouldBe(1);
|
|
}
|
|
}
|