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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/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("/api/upload", stream, "test.xlsx"); // Assert result.IsSuccess.ShouldBeTrue(); result.Value.Id.ShouldBe(1); } }