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