using System.Net; using System.Text; using FluentAssertions; using WebClientTpos.Client.Services; using Xunit; namespace WebClientTpos.ComponentTests.Services; /// /// EN: Unit tests for PosDataService — smart 4-format JSON deserialization and HTTP helpers. /// VI: Unit tests cho PosDataService — deserialize JSON thông minh 4 định dạng và các HTTP helper. /// public class PosDataServiceTests { // EN: Simple item DTO used across tests. // VI: DTO item đơn giản dùng trong các test. private record ItemDto(Guid Id, string Name); // ----------------------------------------------------------------------- // Helper — build PosDataService with a mocked HttpMessageHandler // ----------------------------------------------------------------------- private static (PosDataService sut, AuthStateService authState) BuildSut( string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK) { // EN: Auth is via BFF httpOnly cookie — AuthStateService only tracks email/role/expiry. // VI: Auth qua BFF httpOnly cookie — AuthStateService chỉ theo dõi email/role/expiry. var authState = new AuthStateService(); authState.Login("test@goodgo.vn", "owner"); var handler = new FakeHttpMessageHandler(responseBody, statusCode); var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost/") }; var sut = new PosDataService(httpClient); return (sut, authState); } // ----------------------------------------------------------------------- // GetListFromApiAsync — 4-format deserialization // ----------------------------------------------------------------------- [Fact] public async Task GetListFromApiAsync_PlainArray_ShouldDeserializeCorrectly() { // Arrange — Case 1: plain JSON array var json = """ [ {"id":"00000000-0000-0000-0000-000000000001","name":"Espresso"}, {"id":"00000000-0000-0000-0000-000000000002","name":"Cappuccino"} ] """; var (sut, _) = BuildSut(json); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().HaveCount(2); result[0].Name.Should().Be("Espresso"); } [Fact] public async Task GetListFromApiAsync_PagedResultWrapper_ShouldDeserializeCorrectly() { // Arrange — Case 2: { "items": [...] } var json = """ { "items": [ {"id":"00000000-0000-0000-0000-000000000003","name":"Latte"}, {"id":"00000000-0000-0000-0000-000000000004","name":"Mocha"} ], "totalCount": 2, "pageNumber": 1, "pageSize": 10 } """; var (sut, _) = BuildSut(json); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().HaveCount(2); result[1].Name.Should().Be("Mocha"); } [Fact] public async Task GetListFromApiAsync_ApiResponseWithPagedData_ShouldDeserializeCorrectly() { // Arrange — Case 3: { "data": { "items": [...] } } var json = """ { "success": true, "data": { "items": [ {"id":"00000000-0000-0000-0000-000000000005","name":"Flat White"} ], "totalCount": 1 } } """; var (sut, _) = BuildSut(json); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().HaveCount(1); result[0].Name.Should().Be("Flat White"); } [Fact] public async Task GetListFromApiAsync_ApiResponseWithDirectArray_ShouldDeserializeCorrectly() { // Arrange — Case 4: { "data": [...] } var json = """ { "success": true, "data": [ {"id":"00000000-0000-0000-0000-000000000006","name":"Americano"} ] } """; var (sut, _) = BuildSut(json); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().HaveCount(1); result[0].Name.Should().Be("Americano"); } [Fact] public async Task GetListFromApiAsync_GenericWrapper_ShouldDeserializeFirstArrayProperty() { // Arrange — Case 5: { "products": [...] } (generic single-array-property) var json = """ { "products": [ {"id":"00000000-0000-0000-0000-000000000007","name":"Cold Brew"} ] } """; var (sut, _) = BuildSut(json); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().HaveCount(1); result[0].Name.Should().Be("Cold Brew"); } [Fact] public async Task GetListFromApiAsync_EmptyJson_ShouldReturnEmptyList() { // Arrange var (sut, _) = BuildSut(" "); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().BeEmpty(); } [Fact] public async Task GetListFromApiAsync_NonSuccessStatus_ShouldReturnEmptyList() { // Arrange var (sut, _) = BuildSut("{}", HttpStatusCode.InternalServerError); // Act var result = await sut.GetListFromApiAsync("api/bff/products"); // Assert result.Should().BeEmpty(); } // ----------------------------------------------------------------------- // PostAsync // ----------------------------------------------------------------------- [Fact] public async Task PostAsync_OnSuccess_ShouldReturnTrue() { // Arrange var (sut, _) = BuildSut("{\"success\":true}", HttpStatusCode.OK); // Act var result = await sut.PostAsync("api/bff/orders", new { quantity = 1 }); // Assert result.Should().BeTrue(); } [Fact] public async Task PostAsync_OnBadRequest_ShouldReturnFalse() { // Arrange var (sut, _) = BuildSut("{\"success\":false}", HttpStatusCode.BadRequest); // Act var result = await sut.PostAsync("api/bff/orders", new { quantity = -1 }); // Assert result.Should().BeFalse(); } // ----------------------------------------------------------------------- // PutAsync // ----------------------------------------------------------------------- [Fact] public async Task PutAsync_OnSuccess_ShouldReturnTrue() { var (sut, _) = BuildSut("{}", HttpStatusCode.OK); var result = await sut.PutAsync("api/bff/products/1", new { price = 40000 }); result.Should().BeTrue(); } [Fact] public async Task PutAsync_OnNotFound_ShouldReturnFalse() { var (sut, _) = BuildSut("{}", HttpStatusCode.NotFound); var result = await sut.PutAsync("api/bff/products/nonexistent", new { price = 0 }); result.Should().BeFalse(); } } // --------------------------------------------------------------------------- // Test helper — FakeHttpMessageHandler // --------------------------------------------------------------------------- /// /// EN: In-memory HTTP handler for unit testing HttpClient-based services. /// VI: HTTP handler in-memory để unit test các service dùng HttpClient. /// internal class FakeHttpMessageHandler : HttpMessageHandler { private readonly string _responseBody; private readonly HttpStatusCode _statusCode; public FakeHttpMessageHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK) { _responseBody = responseBody; _statusCode = statusCode; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var response = new HttpResponseMessage(_statusCode) { Content = new StringContent(_responseBody, Encoding.UTF8, "application/json"), }; return Task.FromResult(response); } }