Files
Joseph Doherty 299099f716 test: add POST and bytes tests to ApiClientBaseTests
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.
2026-01-06 11:25:49 -05:00

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