Files
pos-system/apps/web-client-tpos-net/tests/WebClientTpos.ComponentTests/Services/PosDataServiceTests.cs
Ho Ngoc Hai 7a752f4a82 fix(qa): resolve build failures and test issues found in QA verification
- packages/logger: upgrade tsconfig target to ES2022 to support Array.at()
- packages/http-client: exclude test files from tsc build to prevent noUnusedLocals errors
- packages/http-client/test: use vi.hoisted() for mock functions (vi.mock hoisting fix)
- services/goodgo-mcp-server/tests: use vi.hoisted() for all 4 test files (catalog, inventory, analytics, recipe)
- web-client-tpos: remove stale @using WebClientTpos.Client.Components.Auth from 10 auth pages (moved to blazor-ui RCL)
- web-client-tpos: remove AttachToken() calls in PosDataService (auth via BFF httpOnly cookie)
- web-client-tpos: fix IamApiService.SetAuthHeader() and MerchantApiService.AttachTokenAsync() — make no-op, remove _auth dependency
- web-client-tpos: fix Profile.razor — remove AttachToken() method and calls
- web-client-tpos: fix OnboardingReady.razor — escape @keyframes → @@keyframes in Razor style block
- web-client-tpos: fix PosDataService.GetListFromApiAsync() — check array before property lookup to fix plain array deserialization
- web-client-tpos/tests: update AuthStateServiceTests to new AuthStateService.Login(email, role) signature (no token param)
- web-client-tpos/tests: update PosDataServiceTests to new PosDataService(http) constructor (no authState param)

All 113 Node.js tests pass. All 30 .NET component tests pass. All .NET builds succeed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-23 12:07:58 +07:00

263 lines
8.3 KiB
C#

using System.Net;
using System.Text;
using FluentAssertions;
using WebClientTpos.Client.Services;
using Xunit;
namespace WebClientTpos.ComponentTests.Services;
/// <summary>
/// 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.
/// </summary>
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<ItemDto>("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<ItemDto>("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<ItemDto>("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<ItemDto>("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<ItemDto>("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<ItemDto>("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<ItemDto>("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
// ---------------------------------------------------------------------------
/// <summary>
/// EN: In-memory HTTP handler for unit testing HttpClient-based services.
/// VI: HTTP handler in-memory để unit test các service dùng HttpClient.
/// </summary>
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<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(_statusCode)
{
Content = new StringContent(_responseBody, Encoding.UTF8, "application/json"),
};
return Task.FromResult(response);
}
}